NOTE: Generally I will try to cover the latest protocols, but I do review protocols that are not the latest if they are interesting to me. This newsletter serves mostly as a technical review for developers and not an alpha discovery.
Saffron is an asset collateralization platform where liquidity providers have access to dynamic exposure by selecting customized risk and return profiles. According to Saffron Finance’s documentation:
Existing decentralized earning platforms expose liquidity providers to complex code driven outcomes. Network participants must evaluate an array of catastrophic scenarios where the resulting state could wipe out their holdings or lead to significant impermanent loss. It is hard to anticipate the net effect of extreme market volatility or focused economic attacks. Saffron narrows the set of possible outcomes by giving liquidity providers dynamic exposure.
There are six main components to Saffron.
Pools
Capital that is deployed to other DeFi protocols to earn interest.
Adapters
Adapters connect liquidity pools to specific DeFi protocols, such as the DAI/Compound adapter.
Strategy
A strategy connects all pools and adapters and selects the best adapter to deploy capital every hour for each pool.
Epochs
Liquidity is locked up in two week periods. In the end of each epoch interest is distributed to LPs based on their proportional tranche ownership. SFI tokens generated during the epoch can also be redeemed in the same fashion.
SFI
It is the protocol’s native token. The rate of earning depends on how many dollars per second a LP provides to the system during an epoch. There was halving for 8 epochs, and then for the rest of the epochs 200 SFI are generated per epoch until the cap of 100,000 is reached. In the future staking SFI will earn SFI holders a fee generated by protocol users.
Tranches
LPs can add liquidity to 3 user-facing tranches. There are also 2 backend tranches that exist only at the smart contract level to provide additional optionality.
AA Tranche: LPs earn less interest but are covered in case of loss from platform risk. The cover comes from the principal and interest earnings of A tranche LPs. LPs earn 80% from SFI generation.
A Tranche: LPs earn more interest but they lose their principal and interest earnings in case of loss from platform risk. They earn 10% from SFI generation.
S Tranche: LPs earn 10% from SFI generation. Its purpose is to make maintain the tranche interest multiplier at its exact value.
SaffronPool
A Saffron pool stores the following data in order for it to work.
pool_principal: the current principal balance
pool_interest: the current interest balance
tranche_A_multiplier: the current yield multiplier for tranche A
SFI_ratio: Minimum base asset to SFI ratio to join tranche A
best_adapter_address: the best strategy’s adapter address for deploying capital
adapters: a list of adapters
A tranche is an enum.
enum Tranche {S, AA, A}
An epoch is a struct with start date and duration.
struct epoch_params {
uint256 start_date;
uint256 duration;
}
The pool keeps track of its tranches’ principal utilizations and interest/SFI earned.
address[3][] public dsec_token_addresses;
address[3][] public principal_token_addresses;
uint256[3][] public tranche_total_dsec;
uint256[3][] public tranche_total_principal;
uint256[3][] public tranche_total_utilized;
uint256[3][] public tranche_total_unutilized;
uint256[3][] public tranche_S_virtual_utilized;
uint256[3][] public tranche_S_virtual_unutilized;
uint256[3][] public tranche_interest_earned;
uint256[3][] public tranche_SFI_earned;
Creating a new epoch (by governance)
This is done by deploying the contract, then calling the function new_epoch
. It seeds the uint256[3][]
arrays above with [0,0,0]
.
arrayVariable.push([0,0,0]);
It assigns each tranche’s dsec (redeemable for interest + SFI) and principal (redeemable for base asset) token addresses to dsec_token_addresses
and principal_token_addresses
.
/* x can be dsec or principal. */
addressVariable.push([
saffron_LP_x_token_addresses[uint256(Tranche.S)],
saffron_LP_x_token_addresses[uint256(Tranche.AA)],
saffron_LP_x_token_addresses[uint256(Tranche.A)]
]);
It creates the LP token info.
/* x can be A, AA or S. */
/* y can be dsec or principal. */
saffron_LP_token_info[saffron_LP_y_token_addresses[uint256(Tranche.x)]] = SaffronLPTokenInfo({
exists: true,
epoch: epoch,
tranche: Tranche.x,
token_type: LPTokenType.y
});
Adding liquidity to a pool
Users can add liquidity by calling the function add_liquidity
. They decide what amount to deposit and which tranche they want to be in.
function add_liquidity(uint256 amount, Tranche tranche) external override {
}
Currently in v1 users can only deposit to the S or A tranche.
require(tranche == Tranche.S || tranche == Tranche.A, "v1: can't add_liquidity into AA tranche");
A BalanceVars
struct is initialized for the user.
BalanceVars memory bv = BalanceVars({
/* User deposit */
deposit: 0,
/* Capacity for user's intended tranche */
capacity: 0,
/* Change from deposit - capacity */
change: 0,
/* S tranche specific vars */
consumed: 0,
utilized_consumed: 0,
unutilized_consumed: 0,
available_utilized: 0,
available_unutilized: 0
});
The available_utilized
and available_unutilized
balances are calculated by adding the A tranche and the AA tranche’s utilized and unutilized balances, then comparing the numbers with the S tranche’s utilized and unutilized balances. If the S tranche is greater than A and AA tranches combined, then the available utilized/unutilized balances are S - (A+AA). Otherwise, it returns 0.
function get_available_S_balances() public view returns(uint256, uint256) {
uint256 epoch = get_current_epoch();
uint256 AA_A_utilized = tranche_S_virtual_utilized[epoch][uint256(Tranche.A)].add(tranche_S_virtual_utilized[epoch][uint256(Tranche.AA)]);
uint256 AA_A_unutilized = tranche_S_virtual_unutilized[epoch][uint256(Tranche.A)].add(tranche_S_virtual_unutilized[epoch][uint256(Tranche.AA)]);
uint256 S_utilized = tranche_total_utilized[epoch][uint256(Tranche.S)];
uint256 S_unutilized = tranche_total_unutilized[epoch][uint256(Tranche.S)];
return ((S_utilized > AA_A_utilized ? S_utilized - AA_A_utilized : 0), (S_unutilized > AA_A_unutilized ? S_unutilized - AA_A_unutilized : 0));
}
Then, depending on the tranche selected, the following happens.
Tranche S
Add deposit amount to the S tranche’s total unutilized capital.
tranche_total_unutilized[epoch][uint256(Tranche.S)] = tranche_total_unutilized[epoch][uint256(Tranche.S)].add(amount);
bv.deposit = amount;
Tranche A
It calculates the S tranche’s capacity.
/* capacity = (available utilized capital + available unutilized capital) ÷ tranche A multiplier */ bv.capacity = (bv.available_utilized.add(bv.available_unutilized)).div(tranche_A_multiplier);
The deposit is the deposit amount or the S tranche’s capacity if the deposit amount >= S tranche’s capacity.
bv.deposit = (amount < bv.capacity) ? amount : bv.capacity;
The consumed capital is equal to the deposit times the tranche A multiplier. The utilized consumed capital is the consumed capital if the consumed capital is not greater than the available utilized capital.
bv.consumed = bv.deposit.mul(tranche_A_multiplier); if (bv.consumed <= bv.available_utilized) { bv.utilized_consumed = bv.consumed; }
Otherwise, the utilized and unutilized consumed capital will become the current available utilized capital and the current available utilized capital minus the utilized consumed capital respectively. The S tranche’s virtual unutilized AA tranche will increase by the unutilized consumed capital.
bv.utilized_consumed = bv.available_utilized; bv.unutilized_consumed = bv.consumed.sub(bv.utilized_consumed); tranche_S_virtual_unutilized[epoch][uint256(Tranche.AA)] = tranche_S_virtual_unutilized[epoch][uint256(Tranche.AA)].add(bv.unutilized_consumed);
The dollar per second is equal to the deposit times the number of seconds until the end of the current epoch.
uint256 seconds_until_epoch_end = epoch_cycle.start_date.add(epoch.add(1).mul(epoch_cycle.duration)).sub(block.timestamp); uint256 dsec = bv.deposit.mul(seconds_until_epoch_end);
The deposit amount is added to the principal.
/* Add deposit to the pool's principal */ pool_principal = pool_principal.add(bv.deposit); /* Add deposit to this epoch's principal */ epoch_principal[epoch] = epoch_principal[epoch].add(bv.deposit); /* Add deposit to the selected tranche's principal in this epoch */ tranche_total_principal[epoch][uint256(tranche)] = tranche_total_principal[epoch][uint256(tranche)].add(bv.deposit);
The dsec is added to this tranche’s total dsec in this epoch.
tranche_total_dsec[epoch][uint256(tranche)] = tranche_total_dsec[epoch][uint256(tranche)].add(dsec);
It transfers the base asset from the user to the pool, transfers SFI from the user to the pool if it’s tranche A, and mint principal and dsec LP tokens to the user.
IERC20(base_asset_address).safeTransferFrom(msg.sender, address(this), bv.deposit); if (tranche == Tranche.A) IERC20(SFI_address).safeTransferFrom(msg.sender, address(this), bv.deposit / SFI_ratio); SaffronLPBalanceToken(dsec_token_addresses[epoch][uint256(tranche)]).mint(msg.sender, dsec); SaffronLPBalanceToken(principal_token_addresses[epoch][uint256(tranche)]).mint(msg.sender, bv.deposit);
Deploying capital (by strategy)
The contract SaffronStrategy
can control how to deploy a liquidity pool’s capital. It has a list of pools and adapters to choose from.
address[] public pools;
address[] public adapters;
mapping(address=>uint256) private adapter_indexes;
mapping(uint256=>address) private adapter_addresses;
Currently, the best adapter is the first adapter as there is only one adapter.
function select_best_adapter(address base_asset_address) public view returns(address) {
require(base_asset_address != address(0x0), "can't have an adapter for 0x0 address");
return adapters[0];
}
To deploy a pool’s capital, the function deploy_all_capital
is called, which calls the liquidity pool’s function hourly_strategy
if there is capital in the pool. This can happen at most once every hour.
deploy_interval = 1 hours;
require(block.timestamp >= last_deploy + (deploy_interval), "deploy call too soon" );
last_deploy = block.timestamp;
ISaffronPool pool = ISaffronPool(pools[0]);
IERC20 base_asset = IERC20(pool.get_base_asset_address());
if (base_asset.balanceOf(pools[0]) > 0) pool.hourly_strategy(adapters[0]);
Inside hourly_strategy
, it moves all tranches’ unutilized capital to make it utilized.
/* x represents A, AA and S */
tranche_total_utilized[epoch][uint256(Tranche.x)] = tranche_total_utilized[epoch][uint256(Tranche.x)].add(tranche_total_unutilized[epoch][uint256(Tranche.x)]);
tranche_total_unutilized[epoch][uint256(Tranche.x)] = 0;
The deploy amount is added to the adapter’s total principal.
adapter_total_principal = adapter_total_principal.add(amount);
Capital is transferred to the best adapter and deployed. We will take the DAI_Compound_Adapter
as an example. The adapter approves Compound’s cDAI token to spend its DAI and mints cDAI.
IERC20(base_asset_address).safeTransfer(adapter_address, amount);
/* In DAI_Compound_Adapter#deploy_capital... */
DAI.safeApprove(address(cDAI), amount);
uint mint_result = cDAI.mint(amount);
Winding down an epoch (by strategy)
SaffronPool’s wind_down_epoch
function can be called to calculate the interest earned per tranche and closes the epoch.
It calculates SFI earnings per tranche.
/* x represents A, AA and S. */ /* amount_sfi is an argument provided by strategy. */ /* S tranche earns 90% SFI, A tranche earns 10% */ TrancheUint256 public TRANCHE_SFI_MULTIPLIER = TrancheUint256({ S: 90000, AA: 0, A: 10000 }); tranche_SFI_earned[epoch][uint256(Tranche.x)] = TRANCHE_SFI_MULTIPLIER.x.mul(amount_sfi).div(100000);
It calculates the interest earned from the adapter. The adapter’s holding in this scenario is by multiplying its cDAI holdings with its exchange rate as cDAI/DAI’s exchange rate increases every block.
/* In FlatDAICompoundAdapter */ uint256 holdings = cDAI.balanceOf(address(this)).mul(cDAI.exchangeRateCurrent()).div(10**18); if (holdings < principal) { wind_down.epoch_interest = 0; } else { wind_down.epoch_interest = holdings - principal; } /* In Saffron Pool */ pool_interest = pool_interest.add(wind_down.epoch_interest);
It calculates the combined dollar per second between tranche A and S, tranche A’s interest ratio, and the interest for both tranches.
/* total dsec = tranche S's dsec + tranche A's dsec */ /* tranche A interest ratio = tranche A's dsec ÷ total dsec */ /* tranche A interest = total interest x tranche A interest ratio ÷ tranche A multiplier */ /* tranche S interest = total interest - tranche A interest */ wind_down.epoch_dsec = tranche_total_dsec[epoch][uint256(Tranche.S)].add(tranche_total_dsec[epoch][uint256(Tranche.A)]); wind_down.tranche_A_interest_ratio = tranche_total_dsec[epoch][uint256(Tranche.A)].mul(1 ether).div(wind_down.epoch_dsec); wind_down.tranche_A_interest = (wind_down.epoch_interest.mul(wind_down.tranche_A_interest_ratio).div(1 ether)).mul(tranche_A_multiplier); wind_down.tranche_S_interest = wind_down.epoch_interest.sub(wind_down.tranche_A_interest);
It updates the interest earned.
tranche_interest_earned[epoch][uint256(Tranche.S)] = wind_down.tranche_S_interest; tranche_interest_earned[epoch][uint256(Tranche.AA)] = 0; tranche_interest_earned[epoch][uint256(Tranche.A)] = wind_down.tranche_A_interest;
Removing liquidity from a pool
Users can remove their liquidity from a pool by calling the function remove_liquidity
. It withdraws the user’s share of dsec and principal tokens and then burns his LP tokens.
It removes the dsec and SFI earned by the LP from the epoch.
/* total dsec in tranche */ uint256 tranche_dsec = tranche_total_dsec[token_epoch][uint256(token_info.tranche)]; /* dsec share to remove from the tranche */ dsec_percent = (tranche_dsec == 0) ? 0 : dsec_amount.mul(1 ether).div(tranche_dsec); /* interest earned based on dsec percent to remove */ interest_owned = tranche_interest_earned[token_epoch][uint256(token_info.tranche)].mul(dsec_percent) / 1 ether; /* SFI earned based on dsec percent to remove */ SFI_earn = tranche_SFI_earned[token_epoch][uint256(token_info.tranche)].mul(dsec_percent) / 1 ether; /* Remove them from the tranche and the pool */ tranche_interest_earned[token_epoch][uint256(token_info.tranche)] = tranche_interest_earned[token_epoch][uint256(token_info.tranche)].sub(interest_owned); tranche_SFI_earned[token_epoch][uint256(token_info.tranche)] = tranche_SFI_earned[token_epoch][uint256(token_info.tranche)].sub(SFI_earn); tranche_total_dsec[token_epoch][uint256(token_info.tranche)] = tranche_total_dsec[token_epoch][uint256(token_info.tranche)].sub(dsec_amount); pool_interest = pool_interest.sub(interest_owned);
It removes the LP’s principal from the epoch.
/* Remove principal amount from the tranche in the epoch */ tranche_total_principal[token_epoch][uint256(token_info.tranche)] = tranche_total_principal[token_epoch][uint256(token_info.tranche)].sub(principal_amount); /* Remove principal amount from the epoch */ epoch_principal[token_epoch] = epoch_principal[token_epoch].sub(principal_amount); /* Remove principal amount from the pool's principal */ pool_principal = pool_principal.sub(principal_amount); /* Remove principal amount from the total amount invested into the adapter */ adapter_total_principal = adapter_total_principal.sub(principal_amount);
Burn LP tokens, redeem cDAI for DAI and transfer DAI/SFI to user.
/* Burn LP tokens */ sbt.burn(msg.sender, dsec_amount); sbt.burn(msg.sender, principal_amount); /* redeem cDAI for DAI (interest and principal), then send it back to the user */ best_adapter.return_capital(interest_owned, msg.sender); best_adapter.return_capital(principal_amount, msg.sender); /* Transfer SFI to the user */ IERC20(SFI_address).safeTransfer(msg.sender, SFI_earn); IERC20(SFI_address).safeTransfer(msg.sender, SFI_return);
Hello, I was given " DAI S Tranche Interest LP Token Balance = 11,442,486.9342972 " as compensation for a contractual error. However, I could not withdraw it because it was too costly to withdraw it to my wallet. Also, I have no idea if it is an LP that will participate in a pool. Can you tell me what it does? Thanks.