Skip to content
joeflanas edited this page Oct 26, 2024 · 6 revisions

stSYRUP

stSYRUP is a smart contract that represents a token that distributes rewards to its holders. Users that hold SYRUP can stake it in order to receive stSYRUP, and receive their portion of rewards that are distributed in additional SYRUP tokens.

The protocol intends to distribute through rewards a portion of SYRUP token inflation (5% annualized) and SYRUP token buybacks to Stakers. The buybacks will be made from revenues generated by the protocol with the quantum voted on and agreed by the Community.

The main source of revenue for the protocol comes from the collection of management and service fees. Management Fees are the portion of Lender interest that is taken by protocol for facilitating the yield generation while Service Fees are paid by the Borrowers as part of their overall cost of borrowing. All fees accrue on a block by block basis and paid to the protocol through the smart contracts each time a Borrower interest payment is made.

The fees collected by the treasury can then be used to buyback SYRUP, which can be distributed to all Stakers, along with a portion of the inflation, using the following mechanism:

stSyrup implements the ERC-4626 Tokenized Vault Standard and inherits its core functionality from Maple's Revenue Distribution Token (referred to as RDT), which in turn inherits Maple's custom implementation of the ERC-20 standard. You can check out more info on both of these contracts below.

Tokenized Vault Standard

Tokenized vaults have a lack of standardization leading to diverse implementation details. Some various examples include lending markets, aggregators, and interest bearing tokens. This makes integration difficult at the aggregator or plugin layer for protocols which need to be compatible with many standards, and forces each protocol to implement their own adapters which can be error prone and can take up development resources.

The ERC-4626 standard allows for the implementation of a standard API for tokenized vaults representing shares of a single underlying ERC-20 token. This standard is an extension of the ERC-20 token that provides basic functionality for depositing and withdrawing tokens and reading balances.

The main functions used by the standard (assets refers to the underlying asset, while shares represents ownership of the assets):

  • deposit() / mint() - Deposits assets / shares into the vault.
  • withdraw() / redeem() - Withdraws assets / shares from the vault.
  • convertToShares() / convertToAssets() - Converts assets into shares and vice versa.
  • totalAssets() - Calculates the total amount of assets currently stored in the vault.

Revenue Distribution Token

The RevenueDistributionToken contract (referred to as RDT) is Maple's implementation of the Tokenized Vault Standard. It features a linear revenue issuance mechanism, intended to distribute SYRUP rewards to users that have staked their Syrup tokens, and is planned to be used for other fungible, interest-accruing tokens in future smart contract deployments. The code can be found here.

The linear issuance mechanism solves the issue of Stakers entering and exiting at favorable times when large discrete revenue distributions are expected, getting an unfair portion of the rewards. This issuance mechanism accrues value continuously over time instead, so that this exploit vector is no longer possible.

Implementing the standard also allows the contract to conform to a new set of tokens that are used to represent shares of an underlying asset, commonly seen in yield optimization vaults. Implementing the standard will improve composability within DeFi and make it easier for projects and developers familiar with the standard to integrate.

Vesting Schedule

The updateVestingSchedule() function is used to initiate the distribution of SYRUP rewards, and can only be called by the Governor. Once it is called all currently unaccounted SYRUP that is located in the smart contract will start to vest over an arbitrary specified amount of time to all Stakers, proportional to their holding.

Due to the fact that rewards can be distributed many times, and over long vesting periods, it is possible that there are multiple vesting schedules which are being issued at any given point in time. Additionally, these vesting schedules may overlap with one another which adds complexity to estimating the effective vesting rate as a function of elapsed time (in other words, it is gas inefficient).

This is where the concept of an issuance rate is used: the issuanceRate state variable is the aggregation of all currently active vesting schedules. It is denominated as the amount of units of Syrup that are being vested per second to all stakers.

Here is an example of how the vesting schedule is updated after multiple distributions of protocol revenue.

image

The first deposit is performed at t0, scheduled to vest until t2 (Period 1, or P1), depicted by the green arrow. On this deposit, the balance change of the contract is depicted by the purple arrow, and the issuance rate (IR1 in the diagram) is set to depositAmount / (t2 - t0).

The second deposit is performed at t1, scheduled to vest until t3 (Period 2, or P2), depicted by the orange arrow. On this deposit, the balance change of the contract is depicted by the purple arrow. Note that this deposit was made during P1. The projected amount that would have been vested in P1 is shown by the dotted green arrow. In order to calculate the new issuance formula, the totalAssets are calculated at t1, which acts as the y-intercept of the issuance function. The issuance rate (IR2 in the diagram) is set to (depositAmount2 + unvestedAmount) / (t3 - t1).

Ownership

Only the owner of the contract (the Maple DAO or Governor) is permitted to distribute protocol rewards. This is because the vesting algorithm is time-sensitive, and using very large vesting periods when issuing new revenue can cause the vesting schedule to become too diluted.

The owner can also be changed to a different address, and this is performed through a 2-way handshake using the setPendingOwner() and acceptOwnership() functions.

Gasless Deposits

When depositing assets into the contract, the depositWithPermit() and mintWithPermit() functions can be used. These allow the user to perform only one transaction to deposit their assets instead of having to manually approve the transfer of the assets beforehand. This functionality is implemented in accordance with EIP-2612.

ERC-20 Implementation

The RDT contract inherits a lot of its basic functionality from Maple's implementation of the ERC-20 standard which can be found here. This implementation has additional functionalities related to approvals of token transfers:

  • Safe Approvals: Allows updating spending allowances through the use of increaseAllowance() and decreaseAllowance() functions. These are included as a defense against an exploit inherent to the ERC-20 standard.
  • Infinite Approvals: Allows an owner to approve a spender for an arbitrary amount of tokens. This type of approval can be set by using the largest integer (type(uint256).max) as the approval amount, and will never be decreased on transferFrom
  • Gasless Approvals: The permit() function enables gasless approvals, which can reduce the amount of transactions required when interacting with contracts. This is implemented in accordance with EIP-2612.

NOTE: This implementation does not perform any address(0) checks when transferring tokens.

SYRUP Conversion

In the future there may be a need to create a new version of the Syrup token contract, and allow bearers of the old token to migrate to the new one. In order to make this process smoother, the stSYRUP contract comes equipped with a migration mechanism that allows the underlying staking asset to be replaced with another one. This effectively allows the protocol to update the version of the Syrup token that is being distributed as a reward to stakers, without the users having to perform the migration explicitly on their own.

More information on this migration mechanism can be found here and here.

Timelock

A migration cannot be performed immediately, but must first be scheduled for execution by the protocol with the use of the scheduleMigration() function. After this