veCARV(s)
Intro
We are excited to announce the launch of veCARV(s), a new staking token designed to empower our community with flexible staking options and enhanced reward potential. Inspired by the Curve model, veCARV(s) allows users to lock their CARV tokens for varying durations, from one month to three years, with corresponding multipliers that increase the longer the stake is held. This model not only incentivizes long-term commitment but also provides users with the flexibility to choose a staking period that best aligns with their investment strategy. The longer you stake, the higher the reward multiplier, offering up to a 9x boost for a three-year commitment. This tiered multiplier structure ensures that every participant, whether they prefer short-term or long-term staking, can benefit from the veCARV(s) system.
Code: https://github.com/carv-protocol/carv-contracts-alphanet/blob/staking/contracts/staking/veCarvs.sol
Function Description
veCarv(s) is not a standard ERC20 token and does not support
transfer
,approve
,allowance
, ortransferFrom
.The ERC20 interfaces supported by veCarv(s) are:
name
,symbol
,balanceOf
, andtotalSupply
.
Staking
Users obtain veCarv(s) tokens by staking CARV into the veCarv(s) contract. The veCarv(s) tokens are not actually transferred to the user’s address; instead, each time a user calls
balanceOf
, a real-time balance is calculated for the user (the same applies tototalSupply
).The initial amount of veCarv(s) a user receives after staking CARV is calculated as: the amount of CARV * staking duration * staking coefficient (where the staking coefficient is tentatively set at 1/120D, meaning that when the staking duration is 120 days, the user initially receives veCarv(s) at a 1:1 ratio).
A user’s veCarv(s) balance will decay linearly over time (decaying per block, meaning that without new staking, the user’s balance will decrease with each block).
Each time a user stakes, a new position is created for the user, recording the amount of CARV tokens staked and the lock-up expiration time.
There are no restrictions on the amount of CARV a user can stake or the number of positions a user can hold. However, the lock-up duration of each position must be a multiple of the minimum period T.
Users can initiate staking at any time, but the start time of each position in the contract is the start time of the current period T (for example, if T is one week and a user initiates staking on Wednesday, the actual staking start time is Monday at midnight, meaning that the tokens have already decayed partially by the time the staking is initiated).
When a user holds multiple positions, the balance is the sum of all positions, but each position can only be redeemed upon expiration; it cannot be added to or have its duration extended.
Redemption
Users can only redeem positions that have reached their expiration date. The redeemed amount will be equal to the staked amount.
After the user’s position expires, they can either redeem positions individually or combine them for redemption.
Minimum Period T
To calculate
totalSupply
andbalanceOf
, the contract implements a special algorithm (for detailed explanation, refer to the section [Algorithm Description] below). This algorithm introduces the concept of the minimum period T into the contract.T is the smallest unit for contract settlement (settlement can be either automatic or triggered by external assistance; external assistance is optional and will not affect the contract’s functionality if absent). The recommended range for T is 1 day to 1 week.
If T is too short, it will increase the gas costs for users and CARV officials; if T is too long, it will reduce the flexibility of user operations (because the lock-up duration must be a multiple of T, and the start time for each position must align with the start time of a given T).
Interface Description
(This section only covers core function interfaces; non-core interfaces are not described here)
Endpoints | |
---|---|
balanceOf(address user) | Queries the veCarv(s) balance of a specified user. |
balanceOfAt(address user, uint256 timestamp) | Queries the veCarv(s) balance of a specified user at a specific time. Note⚠️: This specified time can be any time in the future or past but does not support times before the contract deployment. |
totalSupply() | Queries the current total supply of veCarv(s). |
totalSupplyAt(uint256 timestamp) | Queries the total supply of veCarv(s) at a specific time.
Note⚠️: The time rules are the same as for |
deposit(uint256 amount, uint256 duration) | The user inputs the amount to stake and the lock-up duration, and the contract creates a position. Note⚠️: The staked amount can be any quantity, but the lock-up duration must be a multiple of the minimum period T. |
withdraw(uint64 positionID) | The user inputs a position ID to redeem an expired position. Multiple positions can be redeemed together (via |
positions(uint64 positionID) | Queries the status of a position by its ID (such as the staked amount, lock-up expiration time, etc.). |
Algorithm Description
What problem does this algorithm solve?
In the process of calculating balanceOf and totalSupply for veCarv(s), there is no actual map that records the balance of each user or the global balance. The smallest unit of storage in the contract is the position. To calculate the real-time totalSupply, the most straightforward method would be to traverse and calculate all positions, which is obviously impractical. This algorithm was developed to address this issue (by allocating a small amount of storage in exchange for reducing the number of read operations during each calculation).
This algorithm draws inspiration from parts of the Curve algorithm and has been modified to better suit the needs of veCarv(s).
How does this algorithm calculate veCarv(s) totalSupply
?
To aid understanding, we establish a two-dimensional coordinate system, with the horizontal axis representing time and the vertical axis representing the quantity of veCarv(s).
First, let's consider the simplest scenario where only one user has created a single position. The relationship between the global veCarv(s) quantity and time is given by: , where
slope
is the rate of decline: , where t
is the time variable, and its valid range is .initialSupply
is the initial veCarv(s) quantity after the user creates the position, as shown in the figure below:At this point, we can easily calculate the
totalSupply
of veCarv(s) at any time between using this formula.However, when the system has multiple positions simultaneously, the graph will look like the one below, where each black solid line represents the decay curve of a single position.
By summing the functions of these three black solid lines, we can obtain the global decay function of
totalSupply
. It's also evident that the slope of the global decay function changes at each time point t1 to t6, which correspond to the start and end times of each position. These are the points where the slope of the global decay curve changes.We can store the global curve in an array of
Point
s, where eachPoint
stores three values:The horizontal coordinate
t
, which represents the time.The vertical coordinate
bias
, which represents the initialtotalSupply
at the current time.The slope, which indicates the rate of decay of the curve until the next
Point
.
With this
Point
array, calculatingtotalSupply
at the current time or at any future time becomes very simple:Suppose we want to calculate
totalSupply
at the time , and the most recentPoint
corresponds to the time .If , the slope of the curve changes as the current active positions expire, so we calculate by iterating forward.
If , we can iterate backward through the
Point
s to find the corresponding interval and then calculate.
Below is an explanation of how this
Point
array is constructed:Define the minimum operation period T. The
Point
s are discrete, and the minimum interval betweenPoint
s is T, but the entire curve is continuous (continuity of the curve means thattotalSupply
andbalanceOf
can be calculated at any time).Initialization: The first
Point
is (0, 0), and the slope is also 0.For each subsequent position's start and end time, a new element is added to the
Point
array. The code is as follows:
Using the above algorithm, we can calculate
totalSupply
at any given time with relatively low cost. Similarly, we can also calculatebalanceOf
(which is thetotalSupply
at the user address level).
Last updated