Skip to content

Commit ee9d822

Browse files
committed
Bond minter and strategies
1 parent 6f19553 commit ee9d822

File tree

13 files changed

+335
-301
lines changed

13 files changed

+335
-301
lines changed

contracts/ACash.sol

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

4-
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
4+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
56
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
67

78
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
89
import {AddressQueue} from "./utils/AddressQueue.sol";
9-
import {BondMinterHelpers} from "./utils/BondMinterHelpers.sol";
1010

1111
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
12-
import {ITranche} from "./interfaces/ITranche.sol";
12+
import {ITranche} from "./interfaces/button-wood/ITranche.sol";
13+
import {IBondController} from "./interfaces/button-wood/IBondController.sol";
1314
import {IBondMinter} from "./interfaces/IBondMinter.sol";
14-
import {IBondController} from "./interfaces/IBondController.sol";
15+
import {IFeeStrategy} from "./interfaces/IFeeStrategy.sol";
16+
import {IYieldStrategy} from "./interfaces/IYieldStrategy.sol";
1517

1618
// TODO:
17-
// 1) Factor fee params and math into external strategy pattern to enable more complex logic in future
18-
// 2) Implement replaceable fee strategies
19-
// 3) log events
20-
contract ACash is ERC20, Ownable {
19+
// 1) log events
20+
contract ACash is ERC20, Initializable, Ownable {
2121
using AddressQueue for AddressQueue.Queue;
22-
using BondMinterHelpers for IBondMinter;
2322
using SafeERC20 for IERC20;
2423
using SafeERC20 for ITranche;
2524

26-
// Used for fee and yield values
27-
uint256 public constant PCT_DECIMALS = 6;
28-
29-
//--- fee strategy parameters
30-
// todo: add setter
31-
// todo: rethink AMPL fee token, it can rebase up and down, alternatively SPOT as fee?
32-
address public feeToken;
33-
// Special note: If mint or burn fee is negative, the other must overcompensate in the positive direction.
34-
// Otherwise, user could extract from fee reserve by constant mint/burn transactions.
35-
int256 public mintFeePct;
36-
int256 public burnFeePct;
37-
int256 public rolloverRewardPct;
38-
39-
//---- bond minter parameters
25+
// minter stores a preset bond config and frequency and mints new bonds when poked
4026
IBondMinter public bondMinter;
41-
uint256 public bondMinterConfigIDX;
42-
mapping (IBondMinter => uint256[]) trancheYields;
4327

28+
// calculates fees
29+
IFeeStrategy public feeStrategy;
30+
31+
// calculates tranche yields
32+
IYieldStrategy public yieldStrategy;
4433

45-
//---- bond queue parameters
4634
// bondQueue is a queue of Bonds, which have an associated number of seniority-based tranches.
4735
AddressQueue.Queue public bondQueue;
4836

@@ -52,20 +40,26 @@ contract ACash is ERC20, Ownable {
5240
//---- ERC-20 parameters
5341
uint8 private immutable _decimals;
5442

55-
5643
// trancheIcebox is a holding area for tranches that are underwater or tranches which are about to mature.
5744
// They can only be rolled over and not burnt
5845
mapping(ITranche => bool) trancheIcebox;
5946

60-
constructor(
61-
string memory name,
62-
string memory symbol,
63-
uint8 decimals_,
64-
IBondMinter bondMinter_,
65-
uint256 bondMinterConfigIDX_,
66-
uint256[] memory bondTrancheYields) ERC20(name, symbol) {
47+
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
6748
_decimals = decimals_;
68-
setBondMinter(bondMinter_, bondMinterConfigIDX_, bondTrancheYields);
49+
}
50+
51+
function init(
52+
IBondMinter bondMinter_,
53+
IYieldStrategy yieldStrategy_,
54+
IFeeStrategy feeStrategy_
55+
) public initializer {
56+
require(address(bondMinter_) != address(0), "Expected new bond minter to be valid");
57+
require(address(yieldStrategy_) != address(0), "Expected new yield strategy to be valid");
58+
require(address(feeStrategy_) != address(0), "Expected new fee strategy to be valid");
59+
60+
bondMinter = bondMinter_;
61+
yieldStrategy = yieldStrategy_;
62+
feeStrategy = feeStrategy_;
6963

7064
bondQueue.init();
7165
}
@@ -85,49 +79,59 @@ contract ACash is ERC20, Ownable {
8579
uint256 trancheCount = mintingBond.trancheCount();
8680
require(trancheAmts.length == trancheCount, "Must specify amounts for every bond tranche");
8781

88-
uint256[] storage yields = trancheYields[bondMinter];
89-
// "System Error: trancheYields size doesn't match bond tranche count."
90-
// assert(yields.length == trancheCount);
91-
92-
uint256 mintAmt = 0;
82+
uint256 totalMintAmt = 0;
9383
for (uint256 i = 0; i < trancheCount; i++) {
94-
mintAmt += yields[i] * trancheAmts[i] / (10 ** PCT_DECIMALS);
95-
9684
(ITranche t, ) = mintingBond.tranches(i);
97-
t.safeTransferFrom(msg.sender, address(this), trancheAmts[i]);
85+
t.safeTransferFrom(_msgSender(), address(this), trancheAmts[i]);
86+
87+
totalMintAmt += yieldStrategy.computeTrancheYield(bondMinter, mintingBond, i, trancheAmts[i]);
9888
}
9989

100-
// transfer in fee
101-
int256 fee = mintFeePct * int256(mintAmt) / int256(10 ** PCT_DECIMALS);
102-
if (fee >= 0) {
103-
IERC20(feeToken).safeTransferFrom(msg.sender, address(this), uint256(fee));
90+
// using SPOT as the fee token
91+
int256 fee = feeStrategy.computeMintFee(totalMintAmt);
92+
uint256 mintAmt = 0;
93+
if(fee >= 0) {
94+
mintAmt = totalMintAmt - uint256(fee);
95+
_mint(address(this), uint256(fee));
10496
} else {
105-
// This is very scary!
106-
IERC20(feeToken).safeTransfer(msg.sender, uint256(-fee));
97+
mintAmt = totalMintAmt + uint256(-fee);
10798
}
108-
109-
// mint spot for user
110-
_mint(msg.sender, mintAmt);
111-
112-
return (mintAmt, fee);
99+
_mint(_msgSender(), mintAmt);
100+
101+
return(mintAmt, fee);
102+
103+
// transfer in fee in non native fee token token
104+
// int256 fee = feeStrategy.computeMintFee(mintAmt);
105+
// IERC20 feeToken = feeStrategy.feeToken();
106+
// if (fee >= 0) {
107+
// feeToken.safeTransferFrom(_msgSender(), address(this), uint256(fee));
108+
// } else {
109+
// // This is very scary!
110+
// feeToken.safeTransfer(_msgSender(), uint256(-fee));
111+
// }
112+
// return (mintAmt, fee);
113113
}
114114

115115

116116
// push new bond into the queue
117-
function advanceMintBond(IBondController newBond) public onlyOwner {
118-
// checks
119-
require(bondMinter.isConfigMatch(bondMinterConfigIDX, newBond), "Expect new bond config to match minter config");
117+
function advanceMintBond(IBondController newBond) public {
118+
require(address(newBond) != bondQueue.head(), "New bond already in queue");
119+
require(bondMinter.isInstance(address(newBond)), "Expect new bond to be minted by the minter");
120120
require(newBond.maturityDate() > tolarableBondMaturiyDate(), "New bond matures too soon");
121121

122-
// enqueue empty bond, now minters can use this bond to mint!
123122
bondQueue.enqueue(address(newBond));
124123
}
125124

126-
// todo: make this iterative to continue dequeue till the tail of the queue
125+
// continue dequeue till the tail of the queue
127126
// has a bond which expires sufficiently out into the future
128-
function advanceBurnBond() public onlyOwner {
129-
IBondController latestBond = IBondController(bondQueue.tail());
130-
if(address(latestBond) != address(0) && latestBond.maturityDate() <= tolarableBondMaturiyDate()) {
127+
function advanceBurnBond() public {
128+
while(true){
129+
IBondController latestBond = IBondController(bondQueue.tail());
130+
131+
if(address(latestBond) == address(0) || latestBond.maturityDate() > tolarableBondMaturiyDate()) {
132+
break;
133+
}
134+
131135
// pop from queue
132136
bondQueue.dequeue();
133137

@@ -141,45 +145,42 @@ contract ACash is ERC20, Ownable {
141145
}
142146
}
143147

144-
function setBondMinter(IBondMinter bondMinter_, uint256 bondMinterConfigIDX_, uint256[] memory bondTrancheYields) public onlyOwner {
145-
// TODO: consider using custom minter rather than button's
146-
// the current version does not have a instance check function
147-
require(address(bondMinter_) != address(0), "Expected bond minter to be set");
148-
149-
require(bondMinter_.numConfigs() > bondMinterConfigIDX_, "Expected bond minter to be configured");
150-
148+
function setBondMinter(IBondMinter bondMinter_) external onlyOwner {
149+
require(address(bondMinter_) != address(0), "Expected new bond minter to be valid");
151150
bondMinter = bondMinter_;
152-
bondMinterConfigIDX = bondMinterConfigIDX_;
153-
154-
require(bondTrancheYields.length == bondMinter_.trancheCount(bondMinterConfigIDX_), "Must specify yields for every bond tranche");
155-
trancheYields[bondMinter_] = bondTrancheYields;
156151
}
157152

158-
function tolarableBondMaturiyDate() public view returns (uint256) {
159-
return block.timestamp + _tolarableBondMaturiy;
153+
function setYieldStrategy(IYieldStrategy yieldStrategy_) external onlyOwner {
154+
require(address(yieldStrategy_) != address(0), "Expected new yield strategy to be valid");
155+
yieldStrategy = yieldStrategy_;
160156
}
161157

162-
/*
163-
164-
165-
function calcMintFee(uint256[] calldata trancheAmts) view returns (uint256) {
166-
158+
function setFeeStrategy(IFeeStrategy feeStrategy_) external onlyOwner {
159+
require(address(feeStrategy_) != address(0), "Expected new fee strategy to be valid");
160+
feeStrategy = feeStrategy_;
167161
}
168162

163+
function setTolarableBondMaturiy(uint256 tolarableBondMaturiy) external onlyOwner {
164+
_tolarableBondMaturiy = tolarableBondMaturiy;
165+
}
169166

170-
function redeem(uint256 spotAmt) public returns () {
171167

168+
function tolarableBondMaturiyDate() public view returns (uint256) {
169+
return block.timestamp + _tolarableBondMaturiy;
172170
}
173171

174-
function redeemIcebox(address bond, uint256 trancheAmts) returns () {
172+
/*
173+
function redeem(uint256 spotAmt) public returns () {
175174
176-
}
175+
}
177176
178-
function rollover() public returns () {
177+
function redeemIcebox(address bond, uint256 trancheAmts) returns () {
179178
180-
}
179+
}
181180
181+
function rollover() public returns () {
182182
183+
}
183184
*/
184185

185186
}

contracts/BondMinter.sol

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
pragma solidity ^0.8.0;
2+
3+
import {IBondFactory} from "./interfaces/button-wood/IBondFactory.sol";
4+
import {IBondMinter} from "./interfaces/IBondMinter.sol";
5+
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
8+
9+
// A class of bonds are uniquely identified by collateralToken, trancheRatios and duration,
10+
// which are hashed to create the bond's config hash
11+
// Based on the provided frequency minter instantiates new bonds for each config when poked
12+
//
13+
// Minor Modification to button wood's version keep track of the bond <-> config reference
14+
// https://github.com/buttonwood-protocol/tranche/blob/main/contracts/bondMinter/
15+
// we want to know given a bond, was it minted by this minter and what is it's config hash
16+
contract BondMinter is IBondMinter, Ownable {
17+
using EnumerableSet for EnumerableSet.Bytes32Set;
18+
19+
// bond factory
20+
IBondFactory public immutable bondFactory;
21+
22+
// mint frequency parameters
23+
uint256 public immutable waitingPeriod;
24+
uint256 public lastMintTimestamp;
25+
26+
// mapping of minted bonds
27+
mapping(address => bool) mintedBonds;
28+
29+
// mapping to get the full bond config from config hash
30+
mapping(bytes32 => IBondMinter.BondConfig) private configHashesToConfigs;
31+
32+
// mapping to get bond config hash from bond address
33+
mapping(address => bytes32) private bondToConfigHashes;
34+
35+
// list of config hashes
36+
EnumerableSet.Bytes32Set private configHashes;
37+
38+
constructor(IBondFactory bondFactory_, uint256 waitingPeriod_) {
39+
bondFactory = bondFactory_;
40+
waitingPeriod = waitingPeriod_;
41+
lastMintTimestamp = 0;
42+
}
43+
44+
// checks if bond has been minted using this minter
45+
function isInstance(address bond) external view override returns (bool) {
46+
return mintedBonds[bond];
47+
}
48+
49+
// gets the bond's config hash
50+
function getConfigHash(address bond) external view override returns (bytes32) {
51+
return bondToConfigHashes[bond];
52+
}
53+
54+
// get the bond's config
55+
function getConfig(address bond) external view override returns (IBondMinter.BondConfig memory) {
56+
return configHashesToConfigs[bondToConfigHashes[bond]];
57+
}
58+
59+
// counts all configs
60+
function numConfigs() public view override returns (uint256) {
61+
return configHashes.length();
62+
}
63+
64+
// gets configs
65+
function bondConfigAt(uint256 index) public view override returns (IBondMinter.BondConfig memory) {
66+
return configHashesToConfigs[configHashes.at(index)];
67+
}
68+
69+
function mintBonds() external override {
70+
require(
71+
block.timestamp - lastMintTimestamp >= waitingPeriod,
72+
"BondMinter: Not enough time has passed since last mint timestamp"
73+
);
74+
lastMintTimestamp = block.timestamp;
75+
76+
for (uint256 i = 0; i < numConfigs(); i++) {
77+
BondConfig memory bondConfig = bondConfigAt(i);
78+
bytes32 configHash = computeHash(bondConfig);
79+
address bond = bondFactory.createBond(
80+
bondConfig.collateralToken,
81+
bondConfig.trancheRatios,
82+
lastMintTimestamp + bondConfig.duration
83+
);
84+
85+
mintedBonds[bond] = true;
86+
bondToConfigHashes[bond] = configHash;
87+
88+
emit BondMinted(bond, configHash);
89+
}
90+
}
91+
92+
function addBondConfig(IBondMinter.BondConfig memory config) external onlyOwner returns (bool) {
93+
bytes32 hash = computeHash(config);
94+
if (configHashes.add(hash)) {
95+
configHashesToConfigs[hash] = config;
96+
emit BondConfigAdded(config);
97+
return true;
98+
}
99+
return false;
100+
}
101+
102+
function removeBondConfig(IBondMinter.BondConfig memory config) external onlyOwner returns (bool) {
103+
bytes32 hash = computeHash(config);
104+
if (configHashes.remove(hash)) {
105+
delete configHashesToConfigs[hash];
106+
emit BondConfigRemoved(config);
107+
return true;
108+
}
109+
return false;
110+
}
111+
112+
function computeHash(IBondMinter.BondConfig memory config) private pure returns (bytes32) {
113+
return keccak256(abi.encode(config.collateralToken, config.trancheRatios, config.duration));
114+
}
115+
}

contracts/FeeStrategy.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//SPDX-License-Identifier: Unlicense
2+
pragma solidity ^0.8.0;
3+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
4+
import { IFeeStrategy } from "./interfaces/IFeeStrategy.sol";
5+
6+
contract FeeStrategy is Ownable, IFeeStrategy {
7+
uint256 public constant PCT_DECIMALS = 6;
8+
9+
// todo: add setters
10+
//--- fee strategy parameters
11+
// IERC20 public override feeToken;
12+
13+
// Special note: If mint or burn fee is negative, the other must overcompensate in the positive direction.
14+
// Otherwise, user could extract from fee reserve by constant mint/burn transactions.
15+
int256 public mintFeePct;
16+
int256 public burnFeePct;
17+
int256 public rolloverRewardPct;
18+
19+
// expected mint token to be have the same number of decimals as the fee token
20+
function computeMintFee(uint256 mintAmt) external view override returns(int256) {
21+
return int256(mintAmt) * mintFeePct / int256(10 ** PCT_DECIMALS);
22+
}
23+
24+
function computeBurnFee(uint256 burnAmt) external view override returns(int256) {
25+
return int256(burnAmt) * burnFeePct / int256(10 ** PCT_DECIMALS);
26+
}
27+
}

0 commit comments

Comments
 (0)