Skip to content
Farhaan edited this page Oct 15, 2024 · 2 revisions

stSyrup

stSyrup is a smart contract that represents a token that entitles its holders to revenues generated by the Maple protocol. Users that hold Syrup can stake it in order to receive stSyrup, and can receive their portion of all protocol revenues in the form of additional Syrup tokens.

The main source of revenue for the protocol comes from the collection of establishment fees. Establishment fees are paid by borrowers as an additional cost of using the Maple platform. They are paid continuously by the borrower over the lifespan of any loan issued on the platform, on top of the regular payments of interest and principal.

The establishment fees are split between the Maple treasury and the Pool Delegate that issued the loan. The establishment fee is currently 1% of the loan principal, and is split 2:1 between the treasury and the pool delegate. The fees collected by the treasury can then be used to issue Syrup buybacks, which can be distributed to all stSyrup holders 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 protocol revenue 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 revenue 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 revenue earned. 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, and in Maple's case, interest and/or revenue bearing tokens. Implementing the standard will improve RDT's composability within DeFi and make it easier for projects and developers familiar with the standard to integrate with RDT.

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 RDT contract will start to vest over an arbitrary specified amount of time to all stakers, proportional to their equity.

Due to the fact that protocol revenue 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 revenue 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 revenue 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 revenue. 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 Migration

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