veCARV(s)
Last updated
Last updated
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:
veCarv(s) is not a standard ERC20 token and does not support transfer
, approve
, allowance
, or transferFrom
.
The ERC20 interfaces supported by veCarv(s) are: name
, symbol
, balanceOf
, and totalSupply
.
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 to totalSupply
).
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.
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.
To calculate totalSupply
and balanceOf
, 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).
(This section only covers core function interfaces; non-core interfaces are not described here)
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 balanceOfAt
.
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 multicall
).
positions(uint64 positionID)
Queries the status of a position by its ID (such as the staked amount, lock-up expiration time, etc.).
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 each Point
stores three values:
The horizontal coordinate t
, which represents the time.
The vertical coordinate bias
, which represents the initial totalSupply
at the current time.
The slope, which indicates the rate of decay of the curve until the next Point
.
With this Point
array, calculating totalSupply
at the current time or at any future time becomes very simple:
Suppose we want to calculate totalSupply
at the time , and the most recent Point
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 between Point
s is T, but the entire curve is continuous (continuity of the curve means that totalSupply
and balanceOf
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 calculate balanceOf
(which is the totalSupply
at the user address level).