Try   HackMD

Solidity Code Review of Rebase Projects [WIP]

This is a report for Solidity code review of following projects:

  1. Ampleforth
  2. Basis Cash
  3. Empty Set Dollars

Ampleforth

Smart contracts GitHub repo: ampleforth/uFragments.

TechStack: Truffle V4, OpenZeppelin V2.

Contracts

  • Ample ERC20 Token with Rebase (code)
  • Orchestrator (code)
  • Supply Policy (code)
  • Median Oracle (code)
  • MultiSigWallet (with 3 owners) (general code)

Ample ERC20

  • Internal token variable is named GON. This token amount for every address remains fixed.
  • A terminology called fragment is introduced. $AMPL balance of every user is GON balance / _gonsPerFragment. Simply changing _gonsPerFragment variable scales balances of everyone.
  • Total GONs is fixed (link).
  • For precision in Market Cap, total $AMPL supply is handled as a variable, rest everything related to $AMPL is derived.
  • During rebase, the Supply Policy gives a delta supply (amount change in $AMPL total supply). Based on +/- sign, the total $AMPL supply variable is updated, and then _gonsPerFragment is calculated based on it.
  • There is a Max supply limit on total $AMPL supply. If rebase causes more supply than max supply, the max supply is taken as new supply.
  • Change in the supply and balances comes with immediate effect on rebase.
  • Since Uniswap always checks for self balance on erc20 contracts, it magically works even if balance of reserve changes. After rebase, a new price will reflect on Uniswap immediately.
  • The rebase mechanism has a guard to prevent flash transactions.

Orchestrator

  • This contract exposes a public function rebase() which performs Rebase operation (by calling policy contract), after that invokes some other contracts to update state.
  • Implements ERC-173 Ownable.
  • Other than that some simple methods that adds/removes the invoking of other contract methods. One of it is Uniswap's sync method.

Supply Policy

This contract has a method (invocable by Orchestrator) that:

  1. collects market prices from oracle
  2. calculates rebase % using market price.
  3. Asks ERC20 to increase/decrease supply by calling a method on it.

Median Oracle

  1. This contract whitelists some addresses (named providers) to submit prices. (Currently about 3 providers available).
  2. The Supply Policy contract calls this during a rebase operation to get the price. This contract gives the median of all prices it has.

Dev Resources

A Rebase Transactions:

0x4affa79dc0ca401a3bd149ba119f35d1ce7dd152822c43c3cff88939ad804909

0xd06448d173bbbbcb5ca9f25253cb18e328bc88382ffd682c9af67b79b41d9a8a

cpi from trace = 111533333333333339908 (same in many transactions)

Questions

Which exchanges are used in the VWAP calculuation?

There is a smart contract for oracle. About three addresses are authorized to submit prices to the oracle. It is not clear who owns these addresses. These addresses can be the representing exchanges that support Ampleforth (Bitfinex, Ethfinex, Uniswap).

What formula determines the % of contraction or expansion?

Relavant code link.

  • A Target Price is calculated using current CPI (from oracle) and base CPI.
  • Current Exchange price is fetched from oracle.
  • % = Exchange price - Target Price / Target Price (can be negative). This % is applied on the total $AMPL supply.

How are all wallet addresses updated in a single atomic transaction?

By changing _gonsPerFragment variable.

balanceOf function returns the division of _gonBalance (which is fixed) and _gonsPerFragment which is kindof the rebase factor.

Additional code specific data?

Available in the Contracts section above.

Basis Cash

Smart contract repo: Basis-Cash/basiscash-protocol.

TechStack: Truffle V5 + Hardhat + Waffle + Ganache.

Contracts

  1. Basis Cash ERC20 (code, Etherscan)
  2. Basis Bond ERC20 (code)
  3. Basis Share ERC20 (code)
  4. Oracle (code, Etherscan)
  5. Boardroom (code, Etherscan)
  6. Treasury (code, Etherscan)
  7. Timelock (code, Etherscan)
  8. Distribution contracts
  9. Vote Proxy (code)

Basis Cash

  1. A normal ERC20 + Burn + Mint (can be performed by operator contract).
  2. Contains a burnFrom. This function is used by operator contract (Treasury) when buying bonds through Treasury contract.
  3. Doesn't contain any more logic like rebase.
  4. On deploy, it mints 1 Token (10^18) on deployer address for "initial Uniswap oracle deployment.". Etherscan tx while creating a pair.

Basis Bond

Same as Cash

Basis Share

Same as Cash

Oracle

  • This contract is used to get TWAP of Basis Cash token for a period for the last 1 day. Recent price changes until update function is called, are not considered.
  • Anyone can call the update function every 24 hours.
  • Working: This contract records a price cumulative from Uniswap, after 24 hrs or so again gets a new price cumulative. It takes difference of these two price cumulative and divides by timestamp difference to get the average price and it writes to storage. This calculated avg price is used in the ecosystem until next 24 hours.
  • Possible Vulnerability: We can buyBonds at a old price (24 hour old), get oracle updated and redeem bonds at current price. When we buyBonds, the oracle is updated but interestingly they are updating the oracle after buying the bond at old price.

Boardroom

  1. This is a staking contract, you can stake Basis Shares anytime and withdraw the principal anytime.
  2. If you stake some shares and a seigniorage allocation takes place, the you get the share of the amount proportion to the amount of total staked on the instant of seigniorage allocation. (Seigniorage allocation is basically some amount of Basis Cash given from Treasury and distributed to stakers proportionally).
  3. Uses ContractGuard abstract contract, which aims to prevent a caller to call again (in same tx or even same block).

TODO: Find reason for use of such a ContractGuard. I mean a tx origin check could be sufficient, but why a single use per block? Update: I don't see a obvious reason. TODO: Look for a reason in their whitepaper or some docs later.

Treasury

This is the contract that has logic to keep Basis cash to $1 peg.

  • Allows to buy basis bonds with basis cash only when price is less than $1. If we are giving x Basis cash, we get 1 / price of basis cash * x Basis bonds.
  • allocateSeigniorage is to be called in 24 hour intervals, if price is over 1.05 USDT(or whatever), then it can be called successfully. It takes total supply of Basis Cash multipled by excess price ($0.05) and uses it as seigniorage and funds treasury. If there are enough funds in treasury ($1000+) then it gives these funds to the Boardroom for distribution to stakers.
  • Allows to redeem basis bonds for basis cash (from treasury balance) only when there are enough funds (basis cash) in treasure and the price is above 1.05. If we give x basis bonds, we get x basis cash.
  • Has a helpful execution price check when buying/redeeming bonds. This might prevent a problem when user would sign tx on UI but oracle gets updated when the tx was mined.
  • Has a guard that forces a wallet address to buy or redeem bonds once in a block.
  • Treasory is an operator and owner for the tokens and board room. Operator and owner of Treasury is Timelock.

Timelock

  • This is a taken from Compound protocol.
  • Schedules transactions to be done after a set delay, so that everyone can see what is happening.

Distribution Contracts

There are lot of distribution contracts of various pools. I'm not clearly sure what they mean, but from the code perspective it appears that people stake DAI on a contract and they get Basis Cash as a reward for doing that, over a period of 5 days. There are a whole lot of pools there.

Vote Proxy

This is a forwarder contract of Boardroom for Snapshots page. To be able to vote on snapshots, users have to purchase shares token and stake it on Boardroom to get the boardroom token (which is used in snapshots).

Questions

How many basis bonds are issued? And when does bond issuance stop?

One can issue as many basis bond tokens as they can, by burning Basis Cash tokens when the price is below $1. The bond issuance stop when price on the oracle is $1 or over.

When do basis shares provide inflationary rewards after basis bonds? How is this determined?

Basis shares need to be deposited in Boardroom contract. When the price of Basis Cash is over $1 on the oracle during calling the allocateSeigniorage fn, the extra market cap of Basis Cash is taken and extra minted as Basis Cash and distributed proportionally to stakers of basis cash.

Are there any formulas to determine bond issuance and bond share dynamic?

Bond issuance depends on avg price of Basis cash on the oracle. If average price is p, and you are giving 1 Basis cash, then you get 1/p Bond tokens. Also you can mint bond tokens only if p < 1.

Bond shares are fixed and they don't get minted. They did an initial minting and then transferred ownership. So there is no way to mint more Shares.

Where does the price oracle for all 3 tokens come from?

It takes prices from Uniswap's priceCumulative (TWAP variable).

Are there any governance subtleties?

There is no Governance yet. The admin of Timelock contract is a wallet address that deployed the contracts. This setup basically doesn't let the admin to act maliciously that easily. Admin puts up a tx proposal, it has a minimum delay of 48 hours after which it can be executed. So doing an exit scam is difficult.

Empty Set Dollars

Smart contract repo: emptysetsquad/dollar

Tech Stack: Truffle (some old version)

Terminology

  • Dollar: This is the ERC20 token that the algorithm intends to be stabilized.
  • Debt: Debts is I think amount of ESD pending to be burned. Debt is something that is increased whenever 100 ESD advance incentive is given and when supply is shrinked. It is decreased while increasing debt (to keep it at max 35% of ESD total supply) and while purchasing coupons.
  • Bonding: Giving ESD to the DAO.
  • Market: Participating in Price movement by Buying tokens.
  • Regulater: Increasing/decreasing supply
  • Governance: Upgrading the implementation address.
  • Staged tokens: Number of ESD deposited to DAO.
  • Bonded tokens: Staged tokens which are committed
  • DAO Balance tokens: When someone bonds, they get DAO balance which is around 10^6 * bonded tokens. [can ask if need clarity]
  • Claimable: Number of coupons that can be redeemed for ESD on first-come first-serve.
  • Phantom tokens: They are not mentioned in the whitepaper. They are only in LP incentivisation pool.

Contracts

Dollar

  • Simple ERC20 + Minter + Burner + Permit

Oracle

TODO: check if second pt is correct

  • Creates pair on Uniswap initially.
  • Fetches price cumulatives from Uniswap to calculate TWAP. The timeElapsed is not a fixed, since this is called by DAO contract through capture().
  • The price saved with a boolean called valid. If counter token (USDC) reserve in uniswap is less than a threshold then it is set as false for the epoch.
  • The main price returning function is capture(), it can only be called by DAO and before returning price it updates oracle.

DAO

  • This is kindof all in one contract: Bonding + Market + Supply Regulater + Governance
  • The function similar to rebase is here called as advance(). Calling this advances epoch and some step instructions. Caller gets 100 Dollar incentive minted on their address.
Bonding
  • People can deposit Dollars into the DAO and get staged balance.
  • People can bond their dollar tokens and get a DAO balance by this formula. This DAO balance is bonded tokens * 10^6 initially and later it depends on ratio of total DAO balance supply / total bonded.
  • On every epoch, the advance's bonding step snapshots total bonded and increment epoch.

TODO: find when this ratio changes. Currently it seems in entire DAO contract these two values (total DAO balance supply and total bonded) only change in bond and unbond function.

Market
  • When price of dollars is less than $1, users can burn dollars and buy coupons. Amount of coupons when price is close to $1 is slightly higher while when price is close to 0, it goes towards infinity. This is calculated by an equation.
  • When coupons are purchased, the dollar amount is subtracted from total debt (since dollar amount was burned).
  • When redeeming coupons, need to wait at least 2 epochs. This transfers dollars from DAO to the user.
  • Coupons are non fungible accross epochs (fungible within an epoch) and can be approved and transferred to other wallet addresses similar to ERC20. But cannot be traded on Uniswap or other general exchanges since they are not ERC20.
  • On every epoch, the advance's market step demonitizes old coupons (90 epochs / 30 days).
Regulator
  • On every epoch, Expands or shrinks supply based on oracle price.
  • Max Supply change limit of increase is 3%, but if there are more coupons than can be redeem, supply increase can go upto 6%.
  • Takes price from oracle, but if protocols initial time (90 epochs since start) then by default price = $1.10, else the price from oracle. If there was some problem getting price from oracle, then it takes price as $1.
Govern
  • Users who have DAO balance can use it to vote for a candidate implementation address.

LP Incentivation Pool

  • Purpose of this is to incentivise people to keep ESD in Uniswap instead of depositing to DAO.
  • Every addresses has these balances: Bonded, Staged, Claimable, Phantom.
  • For every address there is a fluidUntil variable which looks after freeze or not status.
  • Allows to add liquidity to Uniswap by just sending USDC from user while dollar tokens from the reward share of the Pool. This increments bond and phantom balance of user on the pool contract.
  • Staged tokens: User deposits Uniswap LP tokens (Dollar/USDC) to pool contract. There is a onlyFrozen modifier, addresses who have not yet crossed 5 epochs can deposit or withdraw.
  • Bond tokens: Anyone who has staged balance can bond their LP tokens and get bond tokens + phantom tokens. When someone bonds or unbonds, their account is unfreezed for 5 ephochs. When you unbond, you get staged back + claimable, while bonded and phantom tokens are decremented.
  • There is an emergency withdraw, DAO address can withdraw all ERC20s to itself.

Questions

What is the source of the price oracle for ESD?

The oracle utlizes the price culumative from Uniswap to calculate TWAP.

Is there a staging period for unstaking ESD from the DAO?

  • If you deposit, you can withdraw.
  • If you deposit, then bond, and unbond, then you cannot withdraw until the 15 epochs / 5 days.

What is the formula by which bonds are sold to ESD holders?

  • Extra bonds (over basic) are given, that is calculated in this file.

What are the rules by which ESD stakers can earn inflationary rewards?

  • If marketing is done and people are attracted to buy ESD to convert them into coupons and if it drives a bull run and price is over $1 for enough time period, and oracle records the TWAP over $1 then extra coins are minted which can be claimed by coupon holders. If marketing fails, then there are no rewards and after 90 day period the coupons are rendered unclaimable (buying coupons is like investment on success of project in next 90 days). For this to success, traffic to this project should keep increasing based on the curve (if curve mints more coupon tokens than needed to give, its bad).

How does governance and treasury of ESD work?

  • Treasury is a Multisig contract (with 7 owners) which gets funds whenever a advance (rebase) takes place.
  • Governance is about upgrading contract. New implementation is proposed and address that have positive bond balances can vote on it, its on-chain, once enough .

How much liquidity added initially in pair?

Tech Stack for the project

  • Solidity v0.8 (just released with some cool additions)
  • OpenZeppelin v3 (need for a new version)
  • Hardhat Dev Enviornment (modern alternative to truffle)
  • EVM: Hardhat and OpenEthereum (used as preference)
  • Waffle (for writing tests)
  • Ethers.js (web3.js alternative)
  • Typechain (for better tests)
  • Solcover (ensuring enough tests)
  • Tracer (to be built)