[NS2DRP] NS-DROP/MIP22 Token Smart Contract Domain Team Assessment

General Information

This assessment, like the MIP21 assessment, also deviates from the standard smart contract technical assessment format because of the idiosyncratic nature of the RWA collateral types. This assessment is a final technical evaluation of MIP22 prior to deployment in the Maker Protocol, and is intended to be a more thorough assessment of all the features. This assessment may be re-used, in part, for other RWA assessments that follow the pattern of MIP22.

For this assessment, we will be focusing on the New Silver NS2DRP token, as well as the infrastructure for all centrifuge tokens.

Technical Information

  • Does the contract implement the ERC20 token standards? Yes.
  • Decimals: 18.
  • Overflow checks: Yes.
  • Mitigation against allowance race-condition: No, but there is a permit() interface.
  • Upgradeable contract patterns: No.
  • Access control or restriction lists: Yes. There is a memberlist.
  • Non-standard features or behaviors: Yes.
    • Above mentioned memberlist
    • addition of the permit() approval pattern (minor bug in version)
  • Key addresses:
    • The wards can call mint() and depend(). Wards are
      • The deployer address, which cannot be called again (dead)
      • The TinlakeRoot which can call depend() and is controlled by a multisig
      • The Tranche which can call mint()
  • Additional notes:
    • In addition to the NS2DRP token, this collateral comes with its own sandbox (much like MIP21). This comes with authed join(), exit(), draw(), and wipe() calls that allow the owner to call them. We will review these, and other aspects of the architecture below.

Reviewing the Architecture

The core RWA architecture consists of the following contracts:

  • LibNote
  • TinlakeManager

The following are considered out of scope for this assessment, and if needed, are subject to governance approval via an executive vote:

  • DssSpell

LibNote contract

Source code

This is the same LibNote we use in the core of DSS.

TinlakeManager contract

Source code

Vault Operations

  • join(uint)
  • exit(uint)
  • draw(uint)
  • wipe(uint)
join(uint)

Join takes a uint wad as an argument and can only be called by the owner. It only functions if the Vault is in a safe, glad, and live state. The manager transfers from the owner to the manager some wad, gives itself a gem balance in the vat, and calls frob() to place that gem into vat. The join() in this case combines a traditional join without the adapter, and a lock(). This method requires the owner to toss an approval to the manager so it can transfer gem.

exit(uint)

Exit takes a uint wad as an argument and can only be called by the owner. It only functions if the Vault is in a safe, glad, and live state. The manager calls frob() to remove the gem from the vat. The gem balance is removed from the vat and transferred from the manager to the owner. The exit() in this case combines a traditional free() with an exit() to the owner.

draw(uint)

Draw takes a uint wad as an argument and can only be called by the owner. It only functions if the Vault is in a safe, glad, and live state. The manager calculates the rate adjusted amount of wad it needs to frob() from the Vault and then calls DaiJoin.exit() to send that amount of DAI to the owner.

wipe(uint)

Wipe takes a uint wad as an argument and can only be called by the owner. It only functions if the Vault is in a safe, glad, and live state. The manager first transfers DAI from the owner to itself and calls DaiJoin.join() to get a vat DAI balance. It then calculates the rate adjusted amount of wad it needs to repay the Vault. The owner must toss an approval to allow the manager to transfer DAI tokens.

Administration

  • setOwner(address)
  • migrate(address)
setOwner(address)

This allows the current owner to reassign the ownership to another contract. Reminder, the owner can call join(), exit(), draw(), wipe(), and take().

migrate(address)

Periodically, changes may be made to the Maker Protocol that require module upgrades. Some of these changes may impact an interface or add a feature that could be required in the manager. For this reason, the smart contract domain team suggested the addition of migrate(). This function can only be called by a GSM delayed Maker governance to perform the following actions:

  • call vat.hope() on a new manager
  • give the new manager an infinite DAI approval
  • give the new manager an infinite gem approval
  • and cage() the old manager

NOTE: Because this cages the existing manager, it also means the owner could call tell() and take() after. For this reason, it’s best to take the following steps in the same block after a call to migrate():

  • move the vault
  • deny the manager on the vat
  • move the vault and any erc20 balances to a new manager

Liquidation

  • tell()
  • unwind(uint)
  • sink()
  • recover()
tell()

Tell kicks off a liquidation. Typically in MCD this would occur via the cat.bite() or dog.bark(); however, in MIP22 there are much longer liquidations with a chance to recover DAI. tell() starts the process. Once started, tell() liquidates the entire Vault irreversibly changing the state of safe to false. This means, if governance triggers a tell, it must be followed to completion.

unwind(uint)

This is the primary liquidation function. Unwind performs the function of a flip.tend() for the protocol. Anyone can call this function. It will recover DAI, paying down the debt for the Vault and removing the corresponding gem amounts.

sink()

Sink is called when the liquidation process has failed, and we must accrue the bad debt to sin. This function sets glad to false, cleans up the Vault, and adds the balance to bad debt in the system. This bad debt will later be resolved with either the surplus buffer if available, or flop() auctions.

recover(uint)

Recover is a public function similar to unwind(), but it can only be called after the debt has been written off with sink(). This allows any possible long tail of recoverable debt to be collected and sent back to the surplus. We do this here because we already accounted for the remaining debt as bad when sink() was called. This function can be called until the adapter is no longer live() (the result of calling migrate() or cage()).

Global Settlement

  • take(uint)
  • cage()
take(uint)

Take only functions once the adapter has been caged with cage() or the migrate() call. It allows the owner to finish up any pending liquidations. A call to tell() must happen first. Also, see the note in migrate() about the actions one should take after a migration.

cage()

This function can be called by governance after the GSM delay to disable the manager. Only tell() and take() to facilitate a liquidations may be called after a cage().

Additional Comments

This does not use a typical Maker oracle. Instead, it uses a DSValue contract that is periodically updated by governance with the correct price. Since the price of the asset doesn’t change, and is instead related to the amount of debt taken against the position by the owner and the fees, this collateral can go a number of weeks without an update. Just like with MIP21, MIP22 doesn’t currently have an oracle attack surface beyond the DSValue authorization, which is just governance behind the GSM pause.

Contract Risk Summary

There is a medium amount of risk in this contract as the mint() and memberlist are under the control of other contracts. The token, however, is always passed between the Maker Protocol, the owner, and the liquidation pool.

On liquidation, MIP22 liquidates the entire Vault. It remains an open question as to how effective this liquidation will be given it’s a longer term process handled by the given pool.

Similar to MIP21 governance must update the price feed for the Vault.

Supporting Materials

Architecture Diagram

Inheritance Diagram

Contracts Description Table

Contract Type Bases
Function Name Visibility Mutability Modifiers
ERC20 Implementation
rely Public :exclamation: :stop_sign: auth
deny Public :exclamation: :stop_sign: auth
add Internal :lock:
sub Internal :lock:
Public :exclamation: :stop_sign: NO❗️
transfer External :exclamation: :stop_sign: NO❗️
transferFrom Public :exclamation: :stop_sign: NO❗️
mint External :exclamation: :stop_sign: auth
burn External :exclamation: :stop_sign: NO❗️
approve External :exclamation: :stop_sign: NO❗️
push External :exclamation: :stop_sign: NO❗️
pull External :exclamation: :stop_sign: NO❗️
move External :exclamation: :stop_sign: NO❗️
permit External :exclamation: :stop_sign: NO❗️
MemberlistLike_2 Implementation
hasMember Public :exclamation: NO❗️
member Public :exclamation: :stop_sign: NO❗️
RestrictedToken Implementation ERC20
hasMember Public :exclamation: NO❗️
Public :exclamation: :stop_sign: ERC20
depend Public :exclamation: :stop_sign: auth
transferFrom Public :exclamation: :stop_sign: checkMember

Legend

Symbol Meaning
:stop_sign: Function can modify state
:dollar: Function is payable
7 Likes