Skip to content

Commit c757fa7

Browse files
committed
bond minter ineteface
1 parent 136d965 commit c757fa7

File tree

6 files changed

+226
-37
lines changed

6 files changed

+226
-37
lines changed

contracts/ACash.sol

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,163 @@
11
//SPDX-License-Identifier: Unlicense
22
pragma solidity ^0.8.0;
33

4-
import "hardhat/console.sol";
4+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
55
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
66

7-
import "./interfaces/IBondController.sol";
7+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {AddressQueue} from "./utils/AddressQueue.sol";
9+
import {BondMinterHelpers} from "./utils/BondMinterHelpers.sol";
810

9-
import "./utils/AddressQueue.sol";
11+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
12+
import {ITranche} from "./interfaces/ITranche.sol";
13+
import {IBondMinter} from "./interfaces/IBondMinter.sol";
14+
import {IBondController} from "./interfaces/IBondController.sol";
1015

1116
// TODO:
1217
// 1) Factor fee params and math into external strategy pattern to enable more complex logic in future
1318
// 2) Implement replaceable fee strategies
14-
contract ACash is ERC20 {
19+
// 3) log events
20+
contract ACash is ERC20, Ownable {
1521
using AddressQueue for AddressQueue.Queue;
16-
17-
// todo: add setter
18-
address public feeToken;
22+
using BondMinterHelpers for IBondMinter;
23+
using SafeERC20 for IERC20;
1924

2025
// Used for fee and yield values
2126
uint256 public constant PCT_DECIMALS = 6;
2227

28+
//--- fee strategy parameters
29+
// todo: add setter
30+
// todo: rethink AMPL fee token, it can rebase up and down, alternatively SPOT as fee?
31+
address public feeToken;
2332
// Special note: If mint or burn fee is negative, the other must overcompensate in the positive direction.
2433
// Otherwise, user could extract from fee reserve by constant mint/burn transactions.
2534
int256 public mintFeePct;
2635
int256 public burnFeePct;
2736
int256 public rolloverRewardPct;
2837

29-
address public bondFactory;
30-
// bondFactory -> ordered array of tranche yields for SPOT
31-
mapping (address => uint256[]) trancheYields;
38+
//---- bond minter parameters
39+
IBondMinter public bondMinter;
40+
uint256 public bondMinterConfigIDX;
41+
mapping (IBondMinter => uint256[]) trancheYields;
3242

33-
uint8 private immutable _decimals;
3443

44+
//---- bond queue parameters
3545
// bondQueue is a queue of Bonds, which have an associated number of seniority-based tranches.
3646
AddressQueue.Queue public bondQueue;
3747

38-
// bondIcebox is a holding area for tranches that are underwater.
39-
// These are skipped in the general burn/redeem case, but may be manually burned redeemed by address
40-
mapping(address => bool) bondIcebox;
41-
42-
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
48+
// system only keeps bonds which further than the `tolarableBondMaturiy` in the queue
49+
uint256 private _tolarableBondMaturiy;
50+
51+
//---- ERC-20 parameters
52+
uint8 private immutable _decimals;
53+
54+
55+
// trancheIcebox is a holding area for tranches that are underwater or tranches which are about to mature.
56+
// They can only be rolled over and not burnt
57+
mapping(ITranche => bool) trancheIcebox;
58+
59+
constructor(
60+
string memory name,
61+
string memory symbol,
62+
uint8 decimals_,
63+
IBondMinter bondMinter_,
64+
uint256 bondMinterConfigIDX_,
65+
uint256[] memory bondTrancheYields) ERC20(name, symbol) {
4366
_decimals = decimals_;
67+
setBondMinter(bondMinter_, bondMinterConfigIDX_, bondTrancheYields);
68+
4469
bondQueue.init();
45-
console.log("Deploying ACash");
4670
}
4771

72+
4873
function decimals() public view override returns (uint8) {
4974
return _decimals;
5075
}
5176

5277
function mint(uint256[] calldata trancheAmts) external returns (uint256, int256) {
53-
require(bondFactory != address(0), "Error: No bond factory set.");
54-
55-
address mintingBond = bondQueue.tail();
56-
require (mintingBond != address(0), "Error: No active minting bond");
57-
58-
uint256 trancheCount = IBondController(mintingBond).trancheCount();
78+
// assert(bondMinter != address(0));
5979

60-
require(trancheAmts.length == trancheCount, "Must specify amounts for every Bond Tranche.");
80+
IBondController mintingBond = IBondController(bondQueue.tail());
81+
require (address(mintingBond) != address(0), "No active minting bond");
6182

62-
uint256[] storage yields = trancheYields[bondFactory];
83+
uint256 trancheCount = mintingBond.trancheCount();
84+
85+
require(trancheAmts.length == trancheCount, "Must specify amounts for every bond tranche");
86+
87+
uint256[] storage yields = trancheYields[bondMinter];
6388
// "System Error: trancheYields size doesn't match bond tranche count."
6489
assert(yields.length == trancheCount);
6590

6691
uint256 mintAmt = 0;
6792
for (uint256 i = 0; i < trancheCount; i++) {
6893
mintAmt += yields[i] * trancheAmts[i] / (10 ** PCT_DECIMALS);
69-
(ITranche t, ) = IBondController(mintingBond).tranches(i);
70-
IERC20(t).transferFrom(msg.sender, address(this), trancheAmts[i]); // assert or use safe transfer
94+
(ITranche t, ) = mintingBond.tranches(i);
95+
IERC20(address(t)).safeTransferFrom(msg.sender, address(this), trancheAmts[i]); // assert or use safe transfer
7196
}
7297

7398
// transfer in fee
7499
int256 fee = mintFeePct * int256(mintAmt) / int256(10 ** PCT_DECIMALS);
75100
if (fee >= 0) {
76-
IERC20(feeToken).transferFrom(msg.sender, address(this), uint256(fee));// todo: safe versions
101+
IERC20(feeToken).safeTransferFrom(msg.sender, address(this), uint256(fee)); // todo: safe versions
77102
} else {
78103
// This is very scary!
79-
IERC20(feeToken).transfer(msg.sender, uint256(-fee));
104+
IERC20(feeToken).safeTransfer(msg.sender, uint256(-fee));
80105
}
81-
106+
82107
// mint spot for user
83108
_mint(msg.sender, mintAmt);
84109

85110
return (mintAmt, fee);
86111
}
87112

113+
114+
// push new bond into the queue
115+
function advanceMintBond(IBondController newBond) public onlyOwner {
116+
// checks
117+
require(bondMinter.isConfigMatch(bondMinterConfigIDX, newBond), "Expect new bond config to match minter config");
118+
require(newBond.maturityDate() > tolarableBondMaturiyDate(), "New bond matures too soon");
119+
120+
// enqueue empty bond, now minters can use this bond to mint!
121+
bondQueue.enqueue(address(newBond));
122+
}
123+
124+
// todo: make this iterative to continue dequeue till the tail of the queue
125+
// has a bond which expires sufficiently out into the future
126+
function advanceBurnBond() public onlyOwner {
127+
IBondController latestBond = IBondController(bondQueue.tail());
128+
if(address(latestBond) != address(0) && latestBond.maturityDate() <= tolarableBondMaturiyDate()) {
129+
// pop from queue
130+
bondQueue.dequeue();
131+
132+
// push individual tranches into icebox
133+
for(uint256 i = 0; i < latestBond.trancheCount(); i++){
134+
(ITranche t,) = latestBond.tranches(i);
135+
trancheIcebox[t] = true;
136+
}
137+
}
138+
}
139+
140+
function setBondMinter(IBondMinter bondMinter_, uint256 bondMinterConfigIDX_, uint256[] memory bondTrancheYields) public onlyOwner {
141+
// TODO: consider using custom minter rather than button's
142+
// the current version does not have a instance check function
143+
require(address(bondMinter_) != address(0), "Expected bond minter to be set");
144+
145+
require(bondMinter_.numConfigs() > bondMinterConfigIDX_, "Expected bond minter to be configured");
146+
147+
bondMinter = bondMinter_;
148+
bondMinterConfigIDX = bondMinterConfigIDX_;
149+
150+
require(bondTrancheYields.length == bondMinter_.trancheCount(bondMinterConfigIDX_), "Must specify yields for every bond tranche");
151+
trancheYields[bondMinter_] = bondTrancheYields;
152+
}
153+
154+
function tolarableBondMaturiyDate() public view returns (uint256) {
155+
return block.timestamp + _tolarableBondMaturiy;
156+
}
157+
88158
/*
159+
160+
89161
function calcMintFee(uint256[] calldata trancheAmts) view returns (uint256) {
90162
91163
}
@@ -103,9 +175,7 @@ contract ACash is ERC20 {
103175
104176
}
105177
106-
function advanceBond(address bond) public onlyOwner {
107-
// enqueue empty bond
108-
}
178+
109179
*/
110180

111181
}

contracts/interfaces/IBondController.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface IBondController {
1717
event Redeem(address user, uint256[] amounts);
1818
event FeeUpdate(uint256 newFee);
1919

20+
function maturityDate() external view returns (uint256);
21+
2022
function collateralToken() external view returns (address);
2123

2224
function tranches(uint256 i) external view returns (ITranche token, uint256 ratio);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @title BondMinter Interface
3+
* @notice Interface for canonically minting bonds according to a stored vaults of configurations
4+
*/
5+
interface IBondMinter{
6+
struct BondConfig {
7+
address collateralToken;
8+
uint256[] trancheRatios;
9+
uint256 duration;
10+
}
11+
12+
/**
13+
* @notice Event emitted when a new BondConfig is added
14+
* @param collateralToken address of the contract for the collateral
15+
* @param trancheRatios Tranching ratios for the bonds. Must sum up to 1000.
16+
* @param duration Duration of the bond in seconds
17+
*/
18+
event BondConfigAdded(address collateralToken, uint256[] trancheRatios, uint256 duration);
19+
20+
/**
21+
* @notice Event emitted when a BondConfig is removed
22+
* @param collateralToken address of the contract for the collateral
23+
* @param trancheRatios Tranching ratios for the bonds. Must sum up to 1000.
24+
* @param duration Duration of the bond in seconds
25+
*/
26+
event BondConfigRemoved(address collateralToken, uint256[] trancheRatios, uint256 duration);
27+
28+
/**
29+
* @notice Adds new bond configuration to internal list. Emits BondConfigAdded on successful add
30+
* @param collateralToken address of the contract for the collateral
31+
* @param trancheRatios Tranching ratios for the bonds. Must sum up to 1000.
32+
* @param duration Duration of the bond in seconds
33+
* @return true if the bondConfig was added, that is if it wasn't already present.
34+
*/
35+
function addBondConfig(
36+
address collateralToken,
37+
uint256[] memory trancheRatios,
38+
uint256 duration
39+
) external returns (bool);
40+
41+
/**
42+
* @notice Removes bond configuration to internal list
43+
* @param collateralToken address of the contract for the collateral
44+
* @param trancheRatios Tranching ratios for the bonds. Must sum up to 1000.
45+
* @param duration Duration of the bond in seconds
46+
* @return true if the bondConfig was removed, that is if it was present.
47+
*/
48+
function removeBondConfig(
49+
address collateralToken,
50+
uint256[] memory trancheRatios,
51+
uint256 duration
52+
) external returns (bool);
53+
54+
/**
55+
* @notice The number of configs stored in the vault
56+
*/
57+
function numConfigs() external view returns (uint256);
58+
59+
/**
60+
* @notice Returns the bondConfig stored at `index`
61+
*/
62+
function bondConfigAt(uint256 index) external view returns (BondConfig memory);
63+
64+
/**
65+
* @notice Sets the bondFactory
66+
* @param _bondFactory The bondFactory that will be used mint the bonds
67+
*/
68+
function setBondFactory(address _bondFactory) external;
69+
70+
/**
71+
* @notice Sets the waitingPeriod required between minting periods
72+
* @param _waitingPeriod The minimum waiting period (in seconds) between mints
73+
*/
74+
function setWaitingPeriod(uint256 _waitingPeriod) external;
75+
76+
/**
77+
* @notice Iterates over configurations and mints bonds for each using the bondFactory
78+
*/
79+
function mintBonds() external;
80+
}

contracts/utils/AddressQueue.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ library AddressQueue {
2828
return a;
2929
}
3030

31-
function head(Queue storage q) public view returns (address) {
31+
function head(Queue storage q) internal view returns (address) {
3232
return q.queue[q.first];
3333
}
3434

35-
function tail(Queue storage q) public view returns (address) {
35+
function tail(Queue storage q) internal view returns (address) {
3636
return q.queue[q.last];
3737
}
3838

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
pragma solidity ^0.8.0;
2+
3+
import {IBondMinter} from "../interfaces/IBondMinter.sol";
4+
import {IBondController} from "../interfaces/IBondController.sol";
5+
6+
library BondMinterHelpers {
7+
// checks if bond as the same config as the minter config
8+
function isConfigMatch(IBondMinter minter, uint256 configIDX, IBondController bond) internal returns (bool) {
9+
10+
IBondMinter.BondConfig memory config = minter.bondConfigAt(configIDX);
11+
12+
// collateral token mismatch
13+
if(bond.collateralToken() != config.collateralToken){
14+
return false;
15+
}
16+
17+
// tranche ratios mismatch
18+
uint256 trancheCount = bond.trancheCount();
19+
if(trancheCount != config.trancheRatios.length) {
20+
return false;
21+
}
22+
23+
for(uint256 i = 0; i < trancheCount; i++) {
24+
(, uint256 ratio) = bond.tranches(i);
25+
if(ratio != config.trancheRatios[i]) {
26+
return false;
27+
}
28+
}
29+
30+
return true;
31+
}
32+
33+
function trancheCount(IBondMinter minter, uint256 configIDX) internal returns(uint256){
34+
IBondMinter.BondConfig memory config = minter.bondConfigAt(configIDX);
35+
return config.trancheRatios.length;
36+
}
37+
}

test/ACash.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ethers } from "hardhat";
33

44
describe("ACash", function () {
55
it("Should construct with name and symbol", async function () {
6-
const ACash = await ethers.getContractFactory("ACash");
6+
const ACash = await ethers.getContractFactory("ACash");
77
const acash = await ACash.deploy("Spot Cash", "SPOT", "9");
8-
await acash.deployed();
8+
await acash.deployed();
99

1010
expect(await acash.name()).to.equal("Spot Cash");
1111
expect(await acash.symbol()).to.equal("SPOT");

0 commit comments

Comments
 (0)