-
Notifications
You must be signed in to change notification settings - Fork 12
Bond minter interface #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7fb8c9f
136d965
db3fbef
b3ada95
6f19553
ee9d822
b0663a5
494df50
60ab638
9fd2a87
a56b638
04f4f91
1ebc0f6
47443c0
13a10b1
778c20a
02a06c2
b2a62d2
bd47782
77d80cd
37f9981
638ebdb
9c57c18
3601a6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| node_modules | ||
| artifacts | ||
| cache | ||
| coverage* | ||
| gasReporterOutput.json | ||
| # folders | ||
| artifacts/ | ||
| build/ | ||
| cache/ | ||
| coverage/ | ||
| dist/ | ||
| lib/ | ||
| node_modules/ | ||
| typechain/ | ||
|
|
||
| # files | ||
| coverage.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "arrowParens": "avoid", | ||
| "bracketSpacing": true, | ||
| "endOfLine":"auto", | ||
| "printWidth": 120, | ||
| "singleQuote": false, | ||
| "tabWidth": 2, | ||
| "trailingComma": "all", | ||
| "overrides": [ | ||
| { | ||
| "files": "*.sol", | ||
| "options": { | ||
| "tabWidth": 4 | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,111 +1,220 @@ | ||
| //SPDX-License-Identifier: Unlicense | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "hardhat/console.sol"; | ||
| import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
| import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | ||
| import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; | ||
| import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
|
|
||
| import "./interfaces/IBondController.sol"; | ||
| import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import { AddressQueue } from "./utils/AddressQueue.sol"; | ||
|
|
||
| import "./utils/AddressQueue.sol"; | ||
| import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import { ITranche } from "./interfaces/button-wood/ITranche.sol"; | ||
| import { IBondController } from "./interfaces/button-wood/IBondController.sol"; | ||
| import { IBondIssuer } from "./interfaces/IBondIssuer.sol"; | ||
| import { IFeeStrategy } from "./interfaces/IFeeStrategy.sol"; | ||
| import { IPricingStrategy } from "./interfaces/IPricingStrategy.sol"; | ||
|
|
||
| // TODO: | ||
| // 1) Factor fee params and math into external strategy pattern to enable more complex logic in future | ||
| // 2) Implement replaceable fee strategies | ||
| contract ACash is ERC20 { | ||
| using AddressQueue for *; | ||
| // 1) log events | ||
| contract ACash is ERC20, Initializable, Ownable { | ||
| using AddressQueue for AddressQueue.Queue; | ||
| using SafeERC20 for IERC20; | ||
| using SafeERC20 for ITranche; | ||
|
|
||
| // todo: add setter | ||
| address public feeToken; | ||
| // minter stores a preset bond config and frequency and mints new bonds when poked | ||
| IBondIssuer public bondIssuer; | ||
|
|
||
| // Used for fee and yield values | ||
| uint256 public constant PCT_DECIMALS = 6; | ||
| // calculates fees | ||
| IFeeStrategy public feeStrategy; | ||
|
|
||
| // 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. | ||
| int256 public mintFeePct; | ||
| int256 public burnFeePct; | ||
| int256 public rolloverRewardPct; | ||
| // calculates bond price | ||
| IPricingStrategy public pricingStrategy; | ||
|
|
||
| address public bondFactory; | ||
| // bondFactory -> ordered array of tranche yields for SPOT | ||
| mapping (address => uint256[]) trancheYields; | ||
|
|
||
| uint8 private immutable _decimals; | ||
| // Yield applied on each tranche | ||
| // 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 | ||
| mapping(bytes32 => uint256[]) private _trancheYields; | ||
|
|
||
| // bondQueue is a queue of Bonds, which have an associated number of seniority-based tranches. | ||
| AddressQueue.Queue public bondQueue; | ||
|
|
||
| // bondIcebox is a holding area for tranches that are underwater. | ||
| // These are skipped in the general burn/redeem case, but may be manually burned redeemed by address | ||
| mapping(address => bool) bondIcebox; | ||
|
|
||
| constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { | ||
| // the minimum maturity time in seconds for a bond below which it gets removed from the bond queue | ||
| uint256 private _minQueueMaturiySec; | ||
|
|
||
| // the maximum maturity time in seconds for a bond above which it can't get added into the bond queue | ||
| uint256 private _maxQueueMaturiySec; | ||
|
|
||
| //---- 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; | ||
|
|
||
| // constants | ||
| uint256 public constant YIELD_DECIMALS = 6; | ||
|
|
||
| constructor( | ||
| string memory name, | ||
| string memory symbol, | ||
| uint8 decimals_ | ||
| ) ERC20(name, symbol) { | ||
| _decimals = decimals_; | ||
| } | ||
|
|
||
| function init( | ||
| IBondIssuer bondIssuer_, | ||
| IPricingStrategy pricingStrategy_, | ||
| IFeeStrategy feeStrategy_ | ||
| ) public initializer { | ||
| require(address(bondIssuer_) != address(0), "Expected new bond minter to be valid"); | ||
| require(address(pricingStrategy_) != address(0), "Expected new pricing strategy to be valid"); | ||
| require(address(feeStrategy_) != address(0), "Expected new fee strategy to be valid"); | ||
|
|
||
| bondIssuer = bondIssuer_; | ||
| pricingStrategy = pricingStrategy_; | ||
| feeStrategy = feeStrategy_; | ||
|
|
||
| bondQueue.init(); | ||
| console.log("Deploying ACash"); | ||
| } | ||
|
|
||
| function decimals() public view override returns (uint8) { | ||
| return _decimals; | ||
| } | ||
|
|
||
| function mint(uint256[] calldata trancheAmts) external returns (uint256, int256) { | ||
| require(bondFactory != address(0), "Error: No bond factory set."); | ||
|
|
||
| address mintingBond = bondQueue.tail(); | ||
| require (mintingBond != address(0), "Error: No active minting bond"); | ||
|
|
||
| // Ignore the Z-tranche | ||
| uint256 usableTrancheCount = IBondController(mintingBond).trancheCount() - 1; | ||
| // "System Error: bond minter not set." | ||
| // assert(bondIssuer != address(0)); | ||
|
|
||
| require(trancheAmts.length == usableTrancheCount, "Must specify amounts for every Bond Tranche."); | ||
| IBondController mintingBond = IBondController(bondQueue.tail()); | ||
| require(address(mintingBond) != address(0), "No active minting bond"); | ||
| bytes32 configHash = bondIssuer.configHash(mintingBond); | ||
|
|
||
| uint256[] storage yields = trancheYields[bondFactory]; | ||
| assert(yields == usableTrancheCount, "System Error: trancheYields size doesn't match bond tranche count."); | ||
| uint256 trancheCount = mintingBond.trancheCount(); | ||
| require(trancheAmts.length == trancheCount, "Must specify amounts for every bond tranche"); | ||
|
|
||
| uint256 mintAmt = 0; | ||
| for (uint256 i = 0; i < usableTrancheCount; i++) { | ||
| mintAmt += yields[i] * trancheAmts[i] / (10 ** PCT_DECIMALS); | ||
| (ITranche t, ) = IBondController(mintingBond).tranches(i); | ||
| IERC20(t).transferFrom(msg.sender, this, trancheAmts[i]); // assert or use safe transfer | ||
| for (uint256 i = 0; i < trancheCount; i++) { | ||
| uint256 trancheYield = _trancheYields[configHash][i]; | ||
| if(trancheYield == 0){ | ||
| continue; | ||
| } | ||
|
|
||
| (ITranche t, ) = mintingBond.tranches(i); | ||
| t.safeTransferFrom(_msgSender(), address(this), trancheAmts[i]); | ||
|
|
||
| // get bond price, ie amount of SPOT for trancheAmts[i] amount of t tranches | ||
| mintAmt += (pricingStrategy.getTranchePrice(t, trancheAmts[i]) * trancheYield) / (10**YIELD_DECIMALS); | ||
| } | ||
|
|
||
| int256 fee = feeStrategy.computeMintFee(mintAmt); | ||
| mintAmt = (fee >= 0) ? mintAmt - uint256(fee) : mintAmt; | ||
| _mint(_msgSender(), mintAmt); | ||
| _transferFee(_msgSender(), fee); | ||
|
|
||
| return (mintAmt, fee); | ||
| } | ||
|
|
||
| // push new bond into the queue | ||
| function advanceMintBond(IBondController newBond) public { | ||
| require(address(newBond) != bondQueue.head(), "New bond already in queue"); | ||
| require(bondIssuer.isInstance(newBond), "Expect new bond to be minted by the minter"); | ||
| require(newBond.maturityDate() > minQueueMaturityDate(), "New bond matures too soon"); | ||
| require(newBond.maturityDate() <= maxQueueMaturityDate(), "New bond matures too late"); | ||
|
|
||
| bondQueue.enqueue(address(newBond)); | ||
| } | ||
|
|
||
| // continue dequeue till the tail of the queue | ||
| // has a bond which expires sufficiently out into the future | ||
| function advanceBurnBond() public { | ||
| while (true) { | ||
| IBondController latestBond = IBondController(bondQueue.tail()); | ||
|
|
||
| if (address(latestBond) == address(0) || latestBond.maturityDate() > minQueueMaturityDate()) { | ||
| break; | ||
| } | ||
|
|
||
| // 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also need to emit a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a todo (at the top) to emit events. Will get to that in a later PR .. |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function _transferFee(address payer, int256 fee) internal { | ||
| // todo: pick either implementation | ||
|
|
||
| // transfer in fee | ||
| int256 fee = mintFeePct * int256(mintAmt) / (10 ** int256(PCT_DECIMALS)); | ||
| // using SPOT as the fee token | ||
| if (fee >= 0) { | ||
| IERC20(feeToken).transferFrom(msg.sender, this, fee); // todo: safe versions | ||
| _mint(address(this), uint256(fee)); | ||
| } else { | ||
| // This is very scary! | ||
| IERC20(feeToken).transfer(msg.sender, fee); | ||
| // TODO consider minting spot if the reserve runs out? | ||
| IERC20(address(this)).safeTransfer(payer, uint256(-fee)); | ||
aalavandhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // mint spot for user | ||
| _mint(msg.sender, mintAmt); | ||
|
|
||
| return (mintAmt, fee); | ||
| // 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)); | ||
| // } | ||
| } | ||
|
|
||
| /* | ||
| function calcMintFee(uint256[] calldata trancheAmts) view returns (uint256) { | ||
| function setBondIssuer(IBondIssuer bondIssuer_) external onlyOwner { | ||
| require(address(bondIssuer_) != address(0), "Expected new bond minter to be valid"); | ||
| bondIssuer = bondIssuer_; | ||
| } | ||
|
|
||
| function setPricingStrategy(IPricingStrategy pricingStrategy_) external onlyOwner { | ||
| require(address(pricingStrategy_) != address(0), "Expected new pricing strategy to be valid"); | ||
| pricingStrategy = pricingStrategy_; | ||
| } | ||
|
|
||
| function setFeeStrategy(IFeeStrategy feeStrategy_) external onlyOwner { | ||
| require(address(feeStrategy_) != address(0), "Expected new fee strategy to be valid"); | ||
| feeStrategy = feeStrategy_; | ||
| } | ||
|
|
||
| function redeem(uint256 spotAmt) public returns () { | ||
| function setTolarableBondMaturiy(uint256 minQueueMaturiySec, uint256 maxQueueMaturiySec) external onlyOwner { | ||
| _minQueueMaturiySec = minQueueMaturiySec; | ||
| _maxQueueMaturiySec = maxQueueMaturiySec; | ||
| } | ||
|
|
||
| function setTrancheYields(bytes32 configHash, uint256[] memory yields) external onlyOwner { | ||
| _trancheYields[configHash] = yields; | ||
| } | ||
|
|
||
| function redeemIcebox(address bond, uint256 trancheAmts) returns () { | ||
| function minQueueMaturityDate() public view returns (uint256) { | ||
| return block.timestamp + _minQueueMaturiySec; | ||
| } | ||
|
|
||
| function maxQueueMaturityDate() public view returns (uint256) { | ||
| return block.timestamp + _maxQueueMaturiySec; | ||
| } | ||
|
|
||
| function rollover() public returns () { | ||
| /* | ||
| function redeem(uint256 spotAmt) public returns () { | ||
|
|
||
| } | ||
| } | ||
|
|
||
| function advanceBond(address bond) public onlyOwner { | ||
| // enqueue empty bond | ||
| } | ||
| function redeemIcebox(address bond, uint256 trancheAmts) returns () { | ||
|
|
||
| } | ||
|
|
||
| function rollover() public returns () { | ||
|
|
||
| } | ||
| */ | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.