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
11 changes: 2 additions & 9 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,12 @@ module.exports = {
node: true,
},
plugins: ["@typescript-eslint"],
extends: [
"standard",
"plugin:prettier/recommended",
"plugin:node/recommended",
],
extends: ["standard", "plugin:prettier/recommended", "plugin:node/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
},
rules: {
"node/no-unsupported-features/es-syntax": [
"error",
{ ignores: ["modules"] },
],
"node/no-unsupported-features/es-syntax": ["error", { ignores: ["modules"] }],
},
};
17 changes: 12 additions & 5 deletions .prettierignore
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
17 changes: 17 additions & 0 deletions .prettierrc
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
}
}
]
}
229 changes: 169 additions & 60 deletions contracts/ACash.sol
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to emit a addedToIcebox(t) event so that the app can display the icebox elements to the rotators later.

Copy link
Member Author

Choose a reason for hiding this comment

The 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));
}

// 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 () {

}
*/

}
Loading