diff --git a/contracts/ACash.sol b/contracts/ACash.sol index 1fa6619f..c6344afa 100644 --- a/contracts/ACash.sol +++ b/contracts/ACash.sol @@ -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"; @@ -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; @@ -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. @@ -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, @@ -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"); @@ -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()); @@ -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 { @@ -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_; } @@ -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 () { } */ diff --git a/contracts/BondIssuer.sol b/contracts/BondIssuer.sol index b22b2905..77b579ef 100644 --- a/contracts/BondIssuer.sol +++ b/contracts/BondIssuer.sol @@ -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; @@ -36,7 +35,6 @@ contract BondIssuer is IBondIssuer { bondDuration = bondDuration_; // 4 weeks config = config_; - _configHash = keccak256(abi.encode(config_.collateralToken, config_.trancheRatios)); lastIssueWindowTimestamp = 0; } @@ -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( diff --git a/contracts/FeeStrategy.sol b/contracts/FeeStrategy.sol index 2cdaeeff..b2c23840 100644 --- a/contracts/FeeStrategy.sol +++ b/contracts/FeeStrategy.sol @@ -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. @@ -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; + } } diff --git a/contracts/PricingStrategy.sol b/contracts/PricingStrategy.sol index 64f3be94..3ff09ef2 100644 --- a/contracts/PricingStrategy.sol +++ b/contracts/PricingStrategy.sol @@ -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; } } diff --git a/contracts/interfaces/IBondIssuer.sol b/contracts/interfaces/IBondIssuer.sol index eed8ec2b..5fab9222 100644 --- a/contracts/interfaces/IBondIssuer.sol +++ b/contracts/interfaces/IBondIssuer.sol @@ -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); } diff --git a/contracts/interfaces/IFeeStrategy.sol b/contracts/interfaces/IFeeStrategy.sol index 2e7846e8..74ad9e62 100644 --- a/contracts/interfaces/IFeeStrategy.sol +++ b/contracts/interfaces/IFeeStrategy.sol @@ -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); } diff --git a/contracts/interfaces/IPricingStrategy.sol b/contracts/interfaces/IPricingStrategy.sol index 1c15176f..32ba3929 100644 --- a/contracts/interfaces/IPricingStrategy.sol +++ b/contracts/interfaces/IPricingStrategy.sol @@ -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); } diff --git a/contracts/interfaces/button-wood/IBondController.sol b/contracts/interfaces/button-wood/IBondController.sol index baf65cc4..04793738 100644 --- a/contracts/interfaces/button-wood/IBondController.sol +++ b/contracts/interfaces/button-wood/IBondController.sol @@ -5,6 +5,8 @@ interface IBondController { function maturityDate() external view returns (uint256); + function creationDate() external view returns (uint256); + function tranches(uint256 i) external view returns (ITranche token, uint256 ratio); function trancheCount() external view returns (uint256 count); diff --git a/contracts/interfaces/button-wood/ITranche.sol b/contracts/interfaces/button-wood/ITranche.sol index 01d51519..b630ac81 100644 --- a/contracts/interfaces/button-wood/ITranche.sol +++ b/contracts/interfaces/button-wood/ITranche.sol @@ -3,7 +3,6 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface ITranche is IERC20 { function collateralToken() external view returns (address); - // TODO: wait till this is merged - // https://github.com/buttonwood-protocol/tranche/pull/30 - function bondController() external view returns (address); + // TODO: wait till these have been merged + function bond() external view returns (address); } diff --git a/contracts/utils/AddressQueue.sol b/contracts/utils/AddressQueue.sol index 8efdbe10..58f44dc3 100644 --- a/contracts/utils/AddressQueue.sol +++ b/contracts/utils/AddressQueue.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; library AddressQueue { struct Queue { mapping(uint256 => address) queue; + // todo: you can get rid of the following if the inclusion check isn't required + mapping(address => bool) items; uint256 first; uint256 last; } @@ -16,14 +18,14 @@ library AddressQueue { function enqueue(Queue storage q, address a) internal { q.last += 1; q.queue[q.last] = a; + q.items[a] = true; } function dequeue(Queue storage q) internal returns (address) { require(q.last >= q.first); // non-empty queue - address a = q.queue[q.first]; - delete q.queue[q.first]; + delete q.items[a]; q.first += 1; return a; } @@ -35,4 +37,8 @@ library AddressQueue { function tail(Queue storage q) internal view returns (address) { return q.queue[q.last]; } + + function contains(Queue storage q, address a) internal view returns (bool) { + return q.items[a]; + } } diff --git a/contracts/utils/BondHelpers.sol b/contracts/utils/BondHelpers.sol new file mode 100644 index 00000000..204194cd --- /dev/null +++ b/contracts/utils/BondHelpers.sol @@ -0,0 +1,40 @@ +pragma solidity ^0.8.0; + +import { IBondController } from "../interfaces/button-wood/IBondController.sol"; +import { ITranche } from "../interfaces/button-wood/ITranche.sol"; + +struct BondInfo { + address collateralToken; + ITranche[] tranches; + uint256[] trancheRatios; + uint256 trancheCount; + bytes32 configHash; +} + +library BondHelpers { + // NOTE: this is very gas intensive + // Optimize calls? + function getInfo(IBondController b) internal returns (BondInfo memory) { + BondInfo memory bInfo; + bInfo.collateralToken = b.collateralToken(); + bInfo.trancheCount = b.trancheCount(); + for (uint256 i = 0; i < bInfo.trancheCount; i++) { + (ITranche t, uint256 ratio) = b.tranches(i); + bInfo.tranches[i] = t; + bInfo.trancheRatios[i] = ratio; + } + bInfo.configHash = keccak256(abi.encode(bInfo.collateralToken, bInfo.trancheRatios)); + return bInfo; + } +} + +library BondInfoHelpers { + function getTrancheIndex(BondInfo memory bInfo, ITranche t) internal returns (uint256) { + for (uint256 i = 0; i < bInfo.trancheCount; i++) { + if (bInfo.tranches[i] == t) { + return i; + } + } + require(false, "BondInfoHelpers: Tranche NOT part of bond"); + } +}