Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 103 additions & 53 deletions contracts/ACash.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { AddressQueue } from "./utils/AddressQueue.sol";
import { BondInfo, BondInfoHelpers, BondHelpers } from "./utils/BondHelpers.sol";

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ITranche } from "./interfaces/button-wood/ITranche.sol";
Expand All @@ -17,10 +18,17 @@ import { IPricingStrategy } from "./interfaces/IPricingStrategy.sol";

// TODO:
// 1) log events
contract ACash is ERC20, Initializable, Ownable {
contract PerpetualTranche is ERC20, Initializable, Ownable {
using AddressQueue for AddressQueue.Queue;
using SafeERC20 for IERC20;
using SafeERC20 for ITranche;
using BondHelpers for IBondController;
using BondInfoHelpers for BondInfo;

// events
event TrancheSynced(ITranche t, uint256 balance);

// parameters

// minter stores a preset bond config and frequency and mints new bonds when poked
IBondIssuer public bondIssuer;
Expand All @@ -35,6 +43,7 @@ contract ACash is ERC20, Initializable, Ownable {
// tranche yields is specific to the parent bond's class identified by its config hash
// a bond's class is the combination of the {collateralToken, trancheRatios}
// specified as a fixed point number with YIELD_DECIMALS
// yield is applied on the tranche amounts
mapping(bytes32 => uint256[]) private _trancheYields;

// bondQueue is a queue of Bonds, which have an associated number of seniority-based tranches.
Expand All @@ -49,12 +58,13 @@ contract ACash is ERC20, Initializable, Ownable {
//---- ERC-20 parameters
uint8 private immutable _decimals;

// trancheIcebox is a holding area for tranches that are underwater or tranches which are about to mature.
// They can only be rolled over and not burnt
mapping(ITranche => bool) trancheIcebox;
// record of all tranches currently being held by the system
// used by off-chain services for indexing
mapping(ITranche => bool) public tranches;

// constants
uint256 public constant YIELD_DECIMALS = 6;
uint8 public constant YIELD_DECIMALS = 6;
uint8 public constant PRICE_DECIMALS = 18;

constructor(
string memory name,
Expand Down Expand Up @@ -90,35 +100,83 @@ contract ACash is ERC20, Initializable, Ownable {

IBondController mintingBond = IBondController(bondQueue.tail());
require(address(mintingBond) != address(0), "No active minting bond");
bytes32 configHash = bondIssuer.configHash(mintingBond);

uint256 trancheCount = mintingBond.trancheCount();
require(trancheAmts.length == trancheCount, "Must specify amounts for every bond tranche");
BondInfo memory mintingBondInfo = mintingBond.getInfo();
require(trancheAmts.length == mintingBondInfo.trancheCount, "Must specify amounts for every bond tranche");

uint256 mintAmt = 0;
for (uint256 i = 0; i < trancheCount; i++) {
uint256 trancheYield = _trancheYields[configHash][i];
for (uint256 i = 0; i < mintingBondInfo.trancheCount; i++) {
uint256 trancheYield = _trancheYields[mintingBondInfo.configHash][i];
if (trancheYield == 0) {
continue;
}

(ITranche t, ) = mintingBond.tranches(i);
ITranche t = mintingBondInfo.tranches[i];
t.safeTransferFrom(_msgSender(), address(this), trancheAmts[i]);
syncTranche(t);

// get bond price, ie amount of SPOT for trancheAmts[i] amount of t tranches
mintAmt += (pricingStrategy.getTranchePrice(t, trancheAmts[i]) * trancheYield) / (10**YIELD_DECIMALS);
mintAmt +=
(((trancheAmts[i] * trancheYield) / (10**YIELD_DECIMALS)) * pricingStrategy.computeTranchePrice(t)) /
(10**PRICE_DECIMALS);
}

int256 fee = feeStrategy.computeMintFee(mintAmt);
mintAmt = (fee >= 0) ? mintAmt - uint256(fee) : mintAmt;
_mint(_msgSender(), mintAmt);
_transferFee(_msgSender(), fee);
address feeToken = feeStrategy.feeToken();

// fee in native token and positive, withold mint partly as fee
if (feeToken == address(this) && fee >= 0) {
mintAmt -= uint256(fee);
_mint(address(this), uint256(fee));
} else {
_settleFee(feeToken, _msgSender(), fee);
}

_mint(_msgSender(), mintAmt);
return (mintAmt, fee);
}

// in case an altruistic party wants to increase the collateralization ratio
function burn(uint256 amount) external {
_burn(_msgSender(), amount);
}

function rollover(
ITranche trancheIn,
ITranche trancheOut,
uint256 trancheInAmt
) external returns (uint256) {
IBondController bondIn = IBondController(trancheIn.bond());
IBondController bondOut = IBondController(trancheOut.bond());

require(address(bondIn) == bondQueue.tail(), "Tranche in should be of minting bond");
require(
address(bondOut) == bondQueue.head() || !bondQueue.contains(address(bondOut)),
"Expected tranche out to be of bond from the head of the queue or icebox"
);

BondInfo memory bondInInfo = bondIn.getInfo();
BondInfo memory bondOutInfo = bondOut.getInfo();

uint256 trancheInYield = _trancheYields[bondInInfo.configHash][bondInInfo.getTrancheIndex(trancheIn)];
uint256 trancheOutYield = _trancheYields[bondOutInfo.configHash][bondOutInfo.getTrancheIndex(trancheOut)];
uint256 trancheOutAmt = (((trancheInAmt * trancheInYield) / trancheOutYield) *
pricingStrategy.computeTranchePrice(trancheIn)) / pricingStrategy.computeTranchePrice(trancheOut);

trancheIn.safeTransferFrom(_msgSender(), address(this), trancheInAmt);
syncTranche(trancheIn);

trancheOut.safeTransfer(_msgSender(), trancheOutAmt);
syncTranche(trancheOut);

// reward is -ve fee
int256 reward = feeStrategy.computeRolloverReward(trancheIn, trancheOut, trancheInAmt, trancheOutAmt);
_settleFee(feeStrategy.feeToken(), _msgSender(), -reward);

return trancheOutAmt;
}

// push new bond into the queue
function advanceMintBond(IBondController newBond) public {
function advanceMintBond(IBondController newBond) external {
require(address(newBond) != bondQueue.head(), "New bond already in queue");
require(bondIssuer.isInstance(newBond), "Expect new bond to be minted by the minter");
require(isActiveBond(newBond), "New bond not active");
Expand All @@ -128,7 +186,7 @@ contract ACash is ERC20, Initializable, Ownable {

// continue dequeue till the tail of the queue
// has a bond which expires sufficiently out into the future
function advanceBurnBond() public {
function advanceBurnBond() external {
while (true) {
IBondController latestBond = IBondController(bondQueue.tail());

Expand All @@ -138,37 +196,21 @@ contract ACash is ERC20, Initializable, Ownable {

// pop from queue
bondQueue.dequeue();

// push individual tranches into icebox if they have a balance
for (uint256 i = 0; i < latestBond.trancheCount(); i++) {
(ITranche t, ) = latestBond.tranches(i);
if (t.balanceOf(address(this)) > 0) {
trancheIcebox[t] = true;
}
}
}
}

function _transferFee(address payer, int256 fee) internal {
// todo: pick either implementation

// using SPOT as the fee token
if (fee >= 0) {
_mint(address(this), uint256(fee));
} else {
// This is very scary!
// TODO consider minting spot if the reserve runs out?
IERC20(address(this)).safeTransfer(payer, uint256(-fee));
// can be externally called to register tranches transferred into the system out of turn
// internally called when tranche balances held by this contract change
// used by off-chain indexers to query tranches currently held by the system
function syncTranche(ITranche t) public {
// log events
uint256 trancheBalance = t.balanceOf(address(this));
if (trancheBalance > 0 && !tranches[t]) {
tranches[t] = true;
} else if (trancheBalance == 0) {
delete tranches[t];
}

// transfer in fee in non native fee token token
// IERC20 feeToken = feeStrategy.feeToken();
// if (fee >= 0) {
// feeToken.safeTransferFrom(payer, address(this), uint256(fee));
// } else {
// // This is very scary!
// feeToken.safeTransfer(payer, uint256(-fee));
// }
emit TrancheSynced(t, trancheBalance);
}

function setBondIssuer(IBondIssuer bondIssuer_) external onlyOwner {
Expand All @@ -178,6 +220,7 @@ contract ACash is ERC20, Initializable, Ownable {

function setPricingStrategy(IPricingStrategy pricingStrategy_) external onlyOwner {
require(address(pricingStrategy_) != address(0), "Expected new pricing strategy to be valid");
require(pricingStrategy_.decimals() == PRICE_DECIMALS, "Expected new pricing stragey to use same decimals");
pricingStrategy = pricingStrategy_;
}

Expand All @@ -202,16 +245,23 @@ contract ACash is ERC20, Initializable, Ownable {
bond.maturityDate() < block.timestamp + maxMaturiySec);
}

/*
function redeem(uint256 spotAmt) public returns () {

}

function redeemIcebox(address bond, uint256 trancheAmts) returns () {

// if the fee is +ve, fee is transfered to self from payer
// if the fee is -ve, it's transfered to the payer from self
function _settleFee(
address feeToken,
address payer,
int256 fee
) internal {
if (fee >= 0) {
IERC20(feeToken).safeTransferFrom(payer, address(this), uint256(fee));
} else {
// Dev note: This is a very dangerous operation
IERC20(feeToken).safeTransfer(payer, uint256(-fee));
}
}

function rollover() public returns () {
/*
function redeem(uint256 spotAmt) public returns () {

}
*/
Expand Down
7 changes: 0 additions & 7 deletions contracts/BondIssuer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ contract BondIssuer is IBondIssuer {
uint256 public lastIssueWindowTimestamp;

IBondIssuer.BondConfig public config;
bytes32 private _configHash;

// mapping of issued bonds
mapping(IBondController => bool) public issuedBonds;
Expand All @@ -36,7 +35,6 @@ contract BondIssuer is IBondIssuer {
bondDuration = bondDuration_; // 4 weeks

config = config_;
_configHash = keccak256(abi.encode(config_.collateralToken, config_.trancheRatios));

lastIssueWindowTimestamp = 0;
}
Expand All @@ -46,11 +44,6 @@ contract BondIssuer is IBondIssuer {
return issuedBonds[bond];
}

// returns the config hash of a given bond if issued by this issuer
function configHash(IBondController bond) external view override returns (bytes32) {
return issuedBonds[bond] ? _configHash : bytes32(0);
}

// issues new bond
function issue() external override {
require(
Expand Down
13 changes: 12 additions & 1 deletion contracts/FeeStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
pragma solidity ^0.8.0;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IFeeStrategy } from "./interfaces/IFeeStrategy.sol";
import { ITranche } from "./interfaces/button-wood/ITranche.sol";

contract FeeStrategy is Ownable, IFeeStrategy {
uint256 public constant PCT_DECIMALS = 6;

// todo: add setters
//--- fee strategy parameters
// IERC20 public override feeToken;
address public override feeToken;

// Special note: If mint or burn fee is negative, the other must overcompensate in the positive direction.
// Otherwise, user could extract from fee reserve by constant mint/burn transactions.
Expand All @@ -26,4 +27,14 @@ contract FeeStrategy is Ownable, IFeeStrategy {
function computeBurnFee(uint256 burnAmt) external view override returns (int256) {
return (int256(burnAmt) * burnFeePct) / int256(10**PCT_DECIMALS);
}

function computeRolloverReward(
ITranche trancheIn,
ITranche trancheOut,
uint256 trancheInAmt,
uint256 trancheOutAmt
) external view override returns (int256) {
// TODO
return 0;
}
}
41 changes: 6 additions & 35 deletions contracts/PricingStrategy.sol
Original file line number Diff line number Diff line change
@@ -1,48 +1,19 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IBondController } from "./interfaces/button-wood/IBondController.sol";
import { ITranche } from "./interfaces/button-wood/ITranche.sol";
import { IPricingStrategy } from "./interfaces/IPricingStrategy.sol";
import { IBondIssuer } from "./interfaces/IBondIssuer.sol";

contract PricingStrategy is Ownable, IPricingStrategy {
uint256 public constant PRICE_DECIMALS = 18;

struct TrancheConfig {
IBondController bond;
uint256[] trancheRatios;
uint256 seniorityIDX;
}

// tranche_price => price_fn(tranche) * tranche_amount
function getTranchePrice(ITranche t, uint256 trancheAmt) external view override returns (uint256) {
return (computeTranchePrice(t) * trancheAmt) / (10**PRICE_DECIMALS);
}
uint8 private constant _decimals = 18;

// Tranche pricing function goes here:
function computeTranchePrice(ITranche t) private view returns (uint256) {
// TrancheConfig c = getTrancheConfig(t);
// based on => c.bond.collateralToken(), c.bond.cdr(), c.bond.maturityDate(), c.seniorityIDX
return (10**PRICE_DECIMALS);
// ie number of tranches of type t for 1 collateral token
function computeTranchePrice(ITranche t) public view override returns (uint256) {
return (10**_decimals);
}

// NOTE: this is very gas intensive
// rebuilding the tranche's pricing parameters though the parent bond
// Alternatively the bond issuer can map the tranche to these parameters for efficient recovery
function getTrancheConfig(ITranche t) private view returns (TrancheConfig memory) {
TrancheConfig memory c;
// TODO: this is still to be merged
// https://github.com/buttonwood-protocol/tranche/pull/30
c.bond = IBondController(t.bondController());
uint256 trancheCount = c.bond.trancheCount();
for (uint256 i = 0; i < trancheCount; i++) {
(ITranche u, uint256 ratio) = c.bond.tranches(i);
c.trancheRatios[i] = ratio;
if (t == u) {
c.seniorityIDX = i;
}
}
return c;
function decimals() external view override returns (uint8) {
return _decimals;
}
}
2 changes: 0 additions & 2 deletions contracts/interfaces/IBondIssuer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@ interface IBondIssuer {
function issue() external;

function isInstance(IBondController bond) external view returns (bool);

function configHash(IBondController bond) external view returns (bytes32);
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IFeeStrategy.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { ITranche } from "./button-wood/ITranche.sol";

interface IFeeStrategy {
function feeToken() external view returns (address);

// amount of spot being mint
function computeMintFee(uint256 amount) external view returns (int256);

// amount of spot being burnt
function computeBurnFee(uint256 amount) external view returns (int256);

// amount of tranche to be traded out for given amount of tranche in
function computeRolloverReward(
ITranche trancheIn,
ITranche trancheOut,
uint256 trancheInAmt,
uint256 trancheOutAmt
) external view returns (int256);
}
4 changes: 3 additions & 1 deletion contracts/interfaces/IPricingStrategy.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ITranche } from "./button-wood/ITranche.sol";

interface IPricingStrategy {
function getTranchePrice(ITranche tranche, uint256 trancheAmt) external view returns (uint256);
function computeTranchePrice(ITranche t) external view returns (uint256);

function decimals() external view returns (uint8);
}
Loading