Money On Chain is a suite of smart contracts dedicated to providing a bitcoin-collateralized stable-coin, Dollar On Chain, (DoC); a passive income hodler-targeted token, BitPro (BPRO), and a leveraged Bitcoin investment instrument (BTCX series).
The rationale behind this is that deposits of Rootstock-BTC (RBTC) help collateralize the DoCs, BitPro absorbs the USD-BTC rate fluctuations, and BTC2X is leveraged borrowing value from BitPro and DoC holders, with a daily interest rate being paid to the former.
A bucket (MoCBucket struct) is a Tokens/RBTC grouping abstraction that represents certain state and follows certain rules.
It's identified by a name (currently C0
and X2
).
It has a "balance" of RBTC, DoC, and BitPro.
If it's a leverage (X) bucket, it also stores the balances of the leveraged token (currently only BTC2X) holders (bproxBalances
and activeBalances
).
If it's instead a base bucket, it has a RBTC balance (inrateBag
) from interests accumulated by leveraged instruments allocations, daily processing will move the corresponding daily payment from this "bag" to base bucket balance.
Balance accounting between buckets is articulated by a series of Smart Contracts that constitute the MOC ecosystem.
Is the ratio between the RBTC locked (backing DoCs) and the total amount of RBTC, be it in a particular bucket or the whole system (usually referred as global).
Locked RBTC amount is a result of the amount of DoCs and their price in BTC (BTC/USD rate).
Its value is pegged to one dollar, in the sense that the SC (using Oracle's btc/usd price) will always[^1] return the equivalent amount of Bitcoin to satisfy that convertibility.
It's targeted towards users seeking to avoid crypto's market volatility.
It's implemented as an ERC20 token, it can be traded freely, but minted/burned only by the Moc system.
The more DocS minted, the more BTC2X can be minted, since they are used for leverage.
[^1]: Needs sufficient collateral (coverage > 1) and redeems are only processed during Settlements
It's targeted towards users seeking to hodl Bitcoins and also receive a passive income from it.
It's implemented as an ERC20 token, it can be traded freely, but minted/burned only by the Moc system.
The more BitPros minted (introducing RBTC to the system), the more coverage the system has, since they add value to the system without locking any.
It's targeted towards users looking to profit from long positions in bitcoin, with two times the risk and reward.
Leveraged instruments borrows capital from base bucket (50% in a X2) and pay a daily[^1] rate to it as return.
It can not be traded freely and does not have an ERC20 interface. BTCX positions can be canceled any time though.
[^1]: Actually uses X amount of block that, given the network, will approximate daily intervals.
It's crucial to the system workflow to have an up to date BTC-USD rate price feed to relay on. This is currently achieved by a separate contract so that it can be easily replaced in the future without affecting the MOC system. See BtcPriceProvider.
System state is ruled by the global Coverage value, and it's relation with the Objective Coverage (Cobj), currently set to 4.
Healthy state of the system. Every Token can be minted, and every Token (besides non-free DoCs) can be redeemed.
When the coverage falls below a certain threshold (Cobj), BitPros can no longer be redeemed as a measure to keep the coverage as high as possible.
Additionally, DoCs can't be minted.
When the coverage falls below the next threshold (uTPDU, currently 1.6), BitPros are sold at a discounted price with the intention of pumping more bitcoins into the system.
If the former measures fail and the coverage falls below last threshold ( currently 1.04), the contracts are locked allowing only the redemption of remaining DoCs at the last available price.
Although DoC Tokens can still be transferred freely, BitPro Token on the other hand is permanently paused, as it has lost all of its value.
This state is irreversible, once the liquidation state is achieved on the contract, there is no coming back even if the price and/or coverage recovers.
We distinguish three types of public interactions with the SC:
MoC
contract, although it usually channels the request to a more specif contract, working as a unified Proxy entry point.Token emission (minting/burning) is only allowed to be done through MOC system, users cannot create or destroy tokens directly on the ERC20 contracts.
Can only be minted in exchange for RBTC.
Given an amount of RBTC paid to the contract, the system calculates the corresponding BitPro amount to mint, RBTC and Bitpro balances are added to the base bucket and the new Tokens are sent to the user.
There's a discount sale, below a certain level of coverage (uTPDU, currently 1.6).
This increases coverage, and allows to mint (sent_btc/target_coverage)*btc_price
extra DoCs, assuming the system is in the 'above coverage' state.
Can only be minted in exchange for RBTC.
Given an amount of RBTC paid to the contract, the system calculates the corresponding DoCs amount to mint [^1], RBTC and DoC balances are added to the base bucket and the new Tokens are sent to the user.
[^1]: The contract must be in the 'Above coverage' state, but given the minting itself lowers coverage, the amount of DoCs to be minted is limited by the preservation of this state. (See globalMaxDoc
)
Can only be "minted" in exchange of RBTC.
The process for the minting is as follows:
The interests are discounted from the sent BTC, that is, if a user sends X
BTC, they'll be assigned X - interests
BTC2X equivalent.
A user can "sell" their BitPro back to the contract and recover the corresponding amount of RBTC.
The contract must be in the 'Above coverage' state.
The BitPros and RBTC are simply discounted from the base bucket.
A DoC redeem request can be created to redeem any amount of DoCs, but this will be processed on next settlement.
The intended amount can be greater than user's balance at request time, allowing to, for example, redeem all future user's DoCs regardless of whether their balance increases.
The redemption of DoCs at the settlement is explained in detail in its own section
Only free DoCs can be redeemed outside of the settlement.
Free DoCs are those that remain in the base bucket, that is, they were not transferred to another to provide leverage.
Tokens and their equivalent in RBTC are simply subtracted from the base bucket.
RBTC deposited are sent back to the user, alongside the refunded interests (waiting in inrateBag) for the remaining time until the settlement (not yet charged).
Associated DoCs are moved back to the base bucket.
Leveraged positions are charged by an interest as return for the borrowed value.
On each investment, the total interest to be paid is calculated and pre-allocated in an common interest "bag" (inrateBag) for all payments.
Once a day[^1] the smart contract allows a transaction to move certain amount of RBTC from that bag, to the base bucket itself.
Once executed for the current block span, it gets blocked "waiting" for blocks to be mined until the conditions are meet again.
[^1]: based on a given number of blocks dependent on the network's mining rate.
Similar to daily payments, settlements are a time based recurrent process which relies on block number to allow/reject execution. This is currently intended to execute on 90 day intervals, although it can be adjusted on deploy.
During settlement, two important events take place: deleveraging of leveraged positions, and Doc redeem request processing.
As this process involves array loops, it might require more than on call to complete. That's why it is wrapped on a TASK and the step amount parameter indicates the maximum number of iterations to be performed on each call. Take in account that during the execution (in between calls) some other functions won't be available to execute.
Although the name deleveraging evokes a broader process, in this case it just refers to the "settlement" of all individual leveraged position. At this time, all interests should had been payed (as they depends inversely to days to settlement), so it's a matter of converting the remaining open positions to RBTC and give back the corresponding DoCs borrowed. Under the hood, it simply invokes redeem for each user account. Market will determine whether they had lost or earned money.
As explained in the redeem section, docs are not entirely liquid but need to be programmed to be redeemed (burn DoC and retrieve RBTC). Users enter a list (currently called queue in anticipation of future pagination) waiting to be executed during settlement.
This events simply goes through the aforementioned collection burning the corresponding DoC amount[^1], sending the equivalent RBTC (at the current BTC-USD rate) and obviously updating the bucket balances in the process.
Said collection should be empty at the end of the process.
[^1]: Note that the intended amount it's not validated until processing, so obviously that amount would only be fulfilled if the user actually owns that amount of DoCs. If he has less, all of them will be redeemed.
If the BTC/USD price drops drastically, an none of the incentive mechanisms along the coverage dropping prevents it to cross the liquidation threshold (currently: coverage < 1.04) the system will enter the liquidation state and the liquidation function will be available to be executed.
Although there is an specific method to evaluate liquidation (evalLiquidation
), to guarantee this process is executed, the same logic is evaluated and, if needed, executed in every MoC state changing method. For example, mintDoC, redeemBPro, etc or even settlement itself; every that has the transitionState
modifier actually.
Liquidation process will invalidate the BitPro Token (it cannot be transfer any more) as a precaution measure as it has no more RBTC backing it, it has no value.
It will go thru the burnout address collection and transfer the corresponding RBTC amount to each one accordingly to theirs DoC balances.
As this process involves array loops, it might require more than on call to complete. That's why it is wrapped on a TASK and the step amount parameter indicates the maximum number of iterations to be performed on each call.
Leveraged buckets will get to critical coverage values quicker than the system as a whole, so they will potentially cross liquidation threshold (currently liq = 1.04
) sooner.
Bucket liquidation process is verified on every bucket State Transition (bucketStateTransition
modifier), that being mint/redeem of leveraged (X) instruments. And there is also a public function to evaluate (and trigger if needed): evalBucketLiquidation
.
For a leveraged bucket, reaching coverage liquidation point (~1) means that user's funds had absorbed all the price dropping, so this process just needs to return the borrowed value (to base bucket) and dissolve the position.
By adding the CommissionSplitter contract and set it as the destination of Money on Chain commissions (just as a normal commission destination address), the splitting process can be made.
The CommissionSplitter contract will accummulate commissions until the split()
function is called. At that moment a part of the commissions will be added to Money on Chain reserves using the Collateral Injection functionality and the other part will be sent to a final destination address.
Collateral injection is the operation of adding reserveTokens to the system's reserves without minting RiskPro. This come in handy when reserves are runnning low and there is a need of Stable token minting.
This injection is made by sending funds directly to the main MoC Contract, this will result in executing the fallback function which will update the internal values according to the sent value.
MoC system is a network of cooperative contracts working together to ultimately provide an US dollar pegged ERC20 Token (DoC). In this sense, be can categorize then into 4 categories:
MoC is the main contract of the MoC ecosystem, it's the entry point of almost every public interaction with it and it articulates the main logic and relations between the rest of the contracts.
It is also the only one that receives RBTC and holds the actual value of the system. The only methods marked as payable
belongs to this contract and corresponds with the actual two ways of adding "value" to the system minting BitPro and DoC: - function mintBPro() public payable transitionState() { ... }
- function mintDoc() public payable transitionState() atLeastState(MoCState.States.AboveCobj) { ... }
You'll also notice that many of it's methods just "redirects" to a more specif contract, abstracting it from the msg.sender
and msg.value
; for example:
/**
* @dev Creates or updates the amount of a Doc redeem Request from the msg.sender
* @param docAmount Amount of Docs to redeem on settlement [using dollarPrecision]
*/
function redeemDocRequest(uint256 docAmount) public {
settlement.addRedeemRequest(docAmount, msg.sender);
}
MoC also handles the [System states][#system-states] by a series of modifiers:
modifier atState(MoCState.States _state) {
require(mocState.state() == _state, "Function cannot be called at this state.");
_;
}
modifier atLeastState(MoCState.States _state) {
require(mocState.state() >= _state, "Function cannot be called at this state.");
_;
}
modifier bucketStateTransition(string bucket) {
evalBucketLiquidation(bucket);
_;
}
modifier transitionState()
{
mocState.nextState();
if (mocState.state() == MoCState.States.Liquidated)
liquidation();
else
_;
}
The CommissionSplitter is the contract used to implement Commission Splitting functionality.
The contract have two main properties that can be modified by Governance:
setCommissionAddress(address _commissionAddress)
function.function setMocProportion(uint256 _mocProportion)
.view
functions to access and evaluate it.States public state = States.AboveCobj;
uint256 public globalNB = 0;
uint256 public peg = 1;
uint256 public bproMaxDiscountRate;
uint256 public liq;
uint256 public utpdu;
It also defines the State enum options:
enum States {
// State 0
Liquidated,
// State 1
BProDiscount,
// State 2
BelowCobj,
// State 3
AboveCobj
}
C0
& X2
), this is expected to grow in the future and even be dynamically modified.mapping(string => MoCBucket) internal mocBuckets;
Inherits from: MoCBase
This contract handles settlement logic and stores redeem request collection. It uses both lastProcessedBlock
and blockSpan
to periodically allow one execution.
State:
uint256 internal lastProcessedBlock;
uint256 internal blockSpan;
struct RedeemRequest {
address who;
uint256 amount;
}
RedeemRequest[] private redeemQueue;
uint256 private numElements = 0;
State:
This contracts uses tree variables to keep track of burn out address. A mapping relating the userAddress (sender) with the burnoutAddress called burnoutBook
, an array (burnoutQueue
) that keep tracks of userAddresses that had manifest intent of having this exit (aka had provide a burnout address) and the number of element of that array (numElements
). Note that this is kept as a separate variable (instead of using burnoutQueue.length
) to have flexibility on where we want or not, to shrink the array if needed.
Also note that even if the array seems redundant, it's needed to iterate [^1] on liquidation, which in not possible with a mapping.
mapping(address => address) burnoutBook;
address[] private burnoutQueue;
uint256 private numElements = 0;
[^1]: See # Well known issue and planned improvements for detail on vulnerabilities for array iterations.
Mocks are for testing purposes, inheriting from MoC contracts and overriding of certain methods allows to expose or manipulates data that wouldn't be possible to unit test instead.
MoC system requires many mathematical operations, in this model just the 2 basic operations are used (addition/subtraction, multiplication/division). To protect against overflows, OpenZeppelin SaveMath library is used on any of this operations.
As current RSK EVM version does not "support" decimal values, it's also important to point out that in MoC every value that is mathematically a decimal, it's represented as an integer adjusted by a given value, which is called precision.
For example, let's take coverage formula:
cob = nB / lB
and suppose that on a given time nB=35
and lB=10
, then
cob = 35 / 10 = 3.5
if we look at the MoCHelperLib coverage method:
/**
Coverage = nB / LB
@dev Calculates Coverage
@param nB Total BTC amount [using reservePrecision]
@param lB Locked bitcoins amount [using reservePrecision]
@return Coverage [using coveragePrecision]
**/
function coverage(MocLibConfig storage libConfig, uint256 nB, uint256 lB) public view
returns(uint256) {
if (lB == 0) {
return UINT256_MAX;
}
return nB.mul(libConfig.coveragePrecision).div(lB);
}
We notice that:
reservePrecision
coveragePrecision
nB
with the return coverage nB.mul(libConfig.coveragePrecision)
and then dives by lB
. The order is not trivial, as even if mathematically there is no difference, making the division first would result in precision lost.In our example, supposing coveragePrecision = 1*10^18
cob = nB.mul(1*10^18).div(lB) = 35*10^17
If we invert the operation order:
cob = nB.div(lB).mul(1*10^18) = 3*10^18
===> loosing precision !!!
There are many different precision values, depending on the value "denomination" being used, all of them are statically defined on the Library:
contract MoCHelperLibMock {
...
constructor() public {
mocLibConfig.dollarPrecision = 10 ** 18;
mocLibConfig.reservePrecision = 10 ** 18;
mocLibConfig.coveragePrecision = 10 ** 18;
mocLibConfig.ratePrecision = 10 ** 18;
mocLibConfig.dayPrecision = 1;
}
Even if many are equal, we keep them on separate variables to be able to adjust them accordantly on future formula changes.
Using unsigned int256 as the norm for all values, sets an upper limit to ~76 decimal places, so even if it's ok to multiply 18 precision values a couple of times, we need to be careful not to overflow nor lose precision.
Most of MoC methods signatures specify the expected precision of the given input and return values, as well of internal operation precision cancelling, for example:
// [DISCOUNT] * [COV] / [COV] = [DISCOUNT]
return bproLiqDiscountRate.mul(utpduCovDiff).div(utpduLiqDiff);
where the convention is to indicate the short description of the precision on brackets ([COV] = coveragePrecision).
In general programming, Composition is preferred upon Inherence, but solidity presents new consideration to keep in mind when choosing each pattern. As composition means a new Contract, and that means intra-contracts calls, meaning public methods and confusing msg.sender
scopes; one might be turn into diminish this and prioritize inherence. The problem then arises when contract code grows and deploy fees goes beyond block gas limit.
Managing this equilibrium was not easy, and we think we are far from having the ultimate clearer and efficient solution yet, but using the whitelisted contract address network pattern, gave us an easy and scalable way on which to relay for contract inter-dependencies.
We understand that this solution makes the system vulnerable on deploy stage, as there is a post deploy initilization phase that needs to be atomically completed in other to have the whole system ready, a hook in this process might be able to compromise it. A post deploy integrity check script might a good option to solve this in the future.
Although not recomended, dynamic array looping is needed to be performed on certain functions:
That's why this functions are wrapped on TASKs and might requires more than one call to complete. They receive a step amount parameter indicating the maximum number of iterations to be performed on each call.
Note: Some model simulations had shown ~100 items is close to RSK block gas limit.
MoC contracts subscribes to a governance implementation that allows an external contract to authorize changers to:
Set single parameters values (for example, adjusting commission fee)
Upgrade specific contracts to new versions (for example updating some formula to make it more efficient)
Pause/Un-pause the whole system (intended as temporal halts for future upgrades)
For further detail on Governance mechanism refer to Moc Governance project
nvm install 8.12 && nvm alias default 8.12
npm install
npm install -g ganache-cli;
ganache-cli
docker pull trufflesuite/ganache-cli;
docker run -d -p 8545:8545 trufflesuite/ganache-cli:latest
npm test
npm run coverage
(Truffle suit)[https://github.com/trufflesuite/truffle] is recommended to compile and deploy the contracts. There are a set of scripts to easy this process for the different known environments. Each environment (and network) might defer in its configuration settings, you can adjust this values in the the migrations/config/config.json
file.
At the end of the deployment the addresses of the most relevant contracts will be displayed. If you are interested in another contracts you should look inside some files depending if the contracts is upgradeable or not.
The addresses of the deployed proxies will be in a file called zos.<network-id>.json
. There you will have to look inside the proxies object and look for the address
of the proxy you are interested in. If you are interested in the address of a contract that has not a proxy you should look for it in the prints of the deployment or inside the builds/<contract-name>.json
file.
contracts/interface/BtcPriceProvider.sol
). You can deploy this contract using the oracle
project or (in development) the mock:contracts/mocks/BtcPriceProviderMock.sol
(which is deployed on development migration by default).