makerdao / lockstake

GNU Affero General Public License v3.0
19 stars 11 forks source link

Lockstake Engine

A technical description of the components of the LockStake Engine (LSE).

1. LockstakeEngine

The LockstakeEngine is the main contract in the set of contracts that implement and support the LSE. On a high level, it supports locking MKR in the contract, and using it to:

When withdrawing back the MKR the user has to pay an exit fee.

There is also support for locking and freeing SKY instead of MKR.

System Attributes:

User Functions:

Sequence Diagram:

Below is a diagram of a typical user sequence for winding up an LSE position.

For simplicity it does not include all external messages, internal operations or token interactions.

sequenceDiagram
    Actor user
    participant engine
    participant urn0
    participant delegate0
    participant farm0
    participant vat

    user->>engine: open(0)
    engine-->>urn0: (creation)
    engine-->>user: return `urn0` address

    user->>engine: lock(`user`, 0, 10, 0)
    engine-->>vat: vat.frob(ilk, `urn0`, `urn0`, address(0), 10, 0) // lock collateral

    user->>engine: selectVoteDelegate(`user`, 0, `delegate0`)
    engine-->>delegate0: lock(10)

    user->>engine: selectFarm(`user`, 0, `farm0`, `ref`)
    engine-->>urn0: stake(`farm0`, 10, `ref`)
    urn0-->>farm0: stake(10, `ref`);

    user->>engine: draw(`user`, 0, `user`, 1000)
    engine-->>vat: vat.frob(ilk, `urn0`, address(0), address(this), 0, 1000) // borrow

Multicall:

LockstakeEngine implements a function, which allows batching several function calls.

For example, a typical flow for a user (or an app/front-end) would be to first query index=ownerUrnsCount(usr) off-chain to retrieve the expected index, then use it to perform a multicall sequence that includes open, selectFarm, lock and stake.

This way, locking and farm-staking can be achieved in only 2 transactions (including the token approval).

Note that since the index is first fetched off-chain and there is no support for passing return values between batched calls, there could be race conditions for calling open. For example, open can be called twice by the user (e.g. in two different contexts) with the second ownerUrnsCount query happening before the first open call has been confirmed. This would lead to both calls using the same urn for selectFarm, lock and stake.

To mitigate this, the index parameter for open is used to make sure the multicall transaction creates the intended urn.

Minimal Proxies:

Upon calling open, an urn contract is deployed for each position. The urn contracts are controlled by the engine and represent each user position for farming, delegation and borrowing. This deployment process uses the ERC-1167 minimal proxy pattern, which helps reduce the open gas consumption by around 70%.

Liquidation Callbacks:

The following functions are called from the LockstakeClipper (see below) throughout the liquidation process.

Configurable Parameters:

2. LockstakeClipper

A modified version of the Liquidations 2.0 Clipper contract, which uses specific callbacks to the LockstakeEngine on certain events. This follows the same paradigm which was introduced in proxy-manager-clipper (used for dss-crop-join).

Specifically, the LockstakeEngine is called upon a beginning of an auction (onKick), a sell of collateral (onTake), and when the auction is concluded (onRemove).

The LSE liquidation process differs from the usual liquidations by the fact that it sends the taker callee the collateral (MKR) in the form of ERC20 tokens and not vat.gem.

Exit Fee on Liquidation

For a liquidated position the relative exit fee is burned from the MKR (collateral) leftovers upon completion of the auction. To ensure enough MKR is left, and also prevent incentives for self-liquidation, the ilk's liquidation ratio (mat) must be set high enough. We calculate below the minimal mat (while ignoring parameters resolution for simplicity):

To be able to liquidate we need the vault to be liquidate-able. The point where that happens is: ① ink * price / mat = debt

The debt to be auctioned is enlarged (by the penalty) to debt * chop (where typically chop is 113%). If we assume the auction selling is at market price and that the market price didn't move since the auction trigger, then the amount of collateral sold is: debt * chop / price

Since we need to make sure that only up to (1-fee) of the total collateral is sold (where fee will typically be 15%), we require: ② debt * chop / price < (1-fee) * ink

From ① and ② we get the requirement on mat: mat > chop / (1 - fee)

For the mentioned examples of chop and fee we get: mat > 1.13 / 0.85 ~= 133%

Note that in practice the mat value is expected to be significantly larger and have buffers over this rough calculation. It should take into account market fluctuations and protocol safety, especially considering that the governance token is used as collateral.

Trusted Farms and Reward Tokens

It is assumed that the farm owner is trusted, the reward token implementation is non-malicious, and that the reward token minter/s are not malicious. Therefore, theoretic attacks, in which for example the reward rate is inflated to a point where the farm mechanics block liquidations, are assumed non-feasible.

Liquidation Bark Gas Benchmarks

Delegate: N, Staking: N - 483492 gas
Delegate: Y, Staking: Y, Yays: 1 - 614242 gas
Delegate: Y, Staking: Y, Yays: 5 - 646522 gas
Measured on: https://github.com/makerdao/lockstake/pull/38/commits/046b1a3c684b178dbd4a8dd8b3fb6e036a485115

For reference, a regular collateral bark cost is around 450K.
Source: https://docs.google.com/spreadsheets/d/1ifb9ePno6KHNNGQA8s6u8KG7BRWa7fhUYH3Z5JGOxag/edit#gid=0

Note that the increased gas cost should be taken into consideration when determining liquidation incentives, along with the dust amount.

Configurable Parameters (similar to a regular Clipper):

3. Vote Delegation

3.a. VoteDelegate

The LSE integrates with the current VoteDelegate contracts almost as is. However, there are three changes done:

3.b. VoteDelegateFactory

Since the VoteDelegate code is being modified (as described above), the factory also needs to be re-deployed.

Note that it is important for the LSE to only allow using VoteDelegate contracts from the factory, so it can be made sure that liquidations can not be blocked.

4. Keepers Support

In general participating in MKR liquidations should be pretty straightforward using the existing on-chain liquidity. However there is a small caveat:

Current Makerdao ecosystem keepers expect receiving collateral in the form of vat.gem (usually to a keeper arbitrage callee contract), which they then need to exit to ERC20 from. However the LSE liquidation mechanism sends the MKR directly in the form of ERC20, which requires a slight change in the keepers mode of operation.

For example, keepers using the Maker supplied exchange-callee for Uniswap V2 would need to use a version that gets the gem instead of the gemJoin and does not call gemJoin.exit. Additionaly, the callee might need to convert the MKR to SKY, in case it interacts with the USDS/SKY Uniswap pool.

5. Splitter

The Splitter contract is in charge of distributing the Surplus Buffer funds on each vow.flap to the Smart Burn Engine (SBE) and the LSE's USDS farm. The total amount sent each time is vow.bump.

To accomplish this, it exposes a kick operation to be triggered periodically. Its logic withdraws DAI from the vow and splits it in two parts. The first part (burn) is sent to the underlying flapper contract to be processed by the SBE. The second part (WAD - burn) is distributed as reward to a farm contract. Note that burn == 1 WAD indicates funneling 100% of the DAI to the SBE without sending any rewards to the farm.

When sending DAI to the farm, the splitter also calls farm.notifyRewardAmount to update the farm contract on the new rewards distribution. This resets the farming distribution period to the governance configured duration and sets the rewards rate according to the sent reward amount and rewards leftovers from the previous distribution (in case there are any).

The Splitter implements rate-limiting using a hop parameter.

Configurable Parameters:

6. StakingRewards

The LSE uses a Maker modified version of the Synthetix Staking Reward as the farm for distributing USDS to stakers.

For compatibility with the SBE, the assumption is that the duration of each farming distribution (farm.rewardsDuration) is similar to the flapper's cooldown period (flap.hop). This in practice divides the overall farming reward distribution to a set of smaller non overlapping distributions. It also allows for periods where there is no distribution at all.

The StakingRewards contract setRewardsDuration function was modified to enable governance to change the farming distribution duration even if the previous distribution has not finished. This now supports changing it simultaneously with the SBE cooldown period (through a governance spell).

Configurable Parameters:

General Notes