-
Notifications
You must be signed in to change notification settings - Fork 7
Add initial variableDebtManager and HookAMM #91
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
base: epic/firmv2
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| // SPDX-License-Identifier: MIT License | ||
| pragma solidity 0.8.20; | ||
|
|
||
| interface IERC20 { | ||
| function transfer(address, uint) external returns (bool); | ||
| function transferFrom(address, address, uint) external returns (bool); | ||
| function balanceOf(address) external view returns (uint); | ||
| } | ||
|
|
||
| interface Mintable is IERC20 { | ||
| function burn(uint) external; | ||
| function mint(address, uint) external; | ||
| } | ||
|
|
||
| interface IVariableDebtManager { | ||
| function buyHook() external returns (bool); | ||
| } | ||
|
|
||
| /** | ||
| * @title HookAMM | ||
| * @dev Limited access xy=k AMM designed to be maximally manipulation resistant on behalf of the DebtManager and FiRMv2 markets | ||
| */ | ||
| contract HookAMM { | ||
|
|
||
| Mintable public immutable dbr; | ||
| IERC20 public immutable dola; | ||
| IVariableDebtManager public variableDebtManager; | ||
| address public gov; | ||
| address public pendingGov; | ||
| address public feeRecipient; | ||
| uint public prevK; | ||
| uint public targetK; | ||
| uint public lastKUpdate; | ||
| uint public maxDolaPrice; | ||
| uint public minDolaPrice; | ||
| uint public dbrBuyFee; | ||
| uint public feesAccrued; | ||
|
|
||
| error Invariant(); | ||
|
|
||
| /** | ||
| * @dev Constructor for sDola contract. | ||
| * WARNING: MIN_SHARES will always be unwithdrawable from the vault. Deployer should deposit enough to mint MIN_SHARES to avoid causing user grief. | ||
| * @param _dola Address of the DOLA token. | ||
| * @param _gov Address of the governance. | ||
| * @param _K Initial value for the K variable used in calculations. | ||
| */ | ||
| constructor( | ||
| address _dola, | ||
| address _dbr, | ||
| address _gov, | ||
| address _feeRecipient, | ||
| uint _K | ||
| ) { | ||
| require(_K > 0, "_K must be positive"); | ||
| dbr = Mintable(_dbr); | ||
| dola = IERC20(_dola); | ||
| gov = _gov; | ||
| feeRecipient = _feeRecipient; | ||
| targetK = _K; | ||
| } | ||
|
|
||
| modifier onlyGov() { | ||
| require(msg.sender == gov, "ONLY GOV"); | ||
| _; | ||
| } | ||
|
|
||
| modifier buyHook() { | ||
| variableDebtManager.buyHook(); | ||
| _; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Returns the current value of K, which is a weighted average between prevK and targetK. | ||
| * @return The current value of K. | ||
| */ | ||
| function getK() public view returns (uint) { | ||
| uint duration = 7 days; | ||
| uint timeElapsed = block.timestamp - lastKUpdate; | ||
| if(timeElapsed > duration) { | ||
| return targetK; | ||
| } | ||
| uint targetWeight = timeElapsed; | ||
| uint prevWeight = duration - timeElapsed; | ||
| return (prevK * prevWeight + targetK * targetWeight) / duration; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Calculates the DOLA reserve based on the current DBR reserve. | ||
| * @return The calculated DOLA reserve. | ||
| */ | ||
| function getDolaReserve() public view returns (uint) { | ||
| return dola.balanceOf(address(this)) + getVirtualDolaReserves(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Returns the current DBR reserve as the sum of dbr balance and claimable dbr | ||
| * @return The current DBR reserve. | ||
| */ | ||
| function getDbrReserve() public view returns (uint) { | ||
| return getK() / getDolaReserve(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets a new target K value. | ||
| * @param _K The new target K value. | ||
| */ | ||
| function setTargetK(uint _K) external onlyGov { | ||
| require(_K > getDbrReserve(), "K must be larger than dbr reserve"); | ||
| prevK = getK(); | ||
| targetK = _K; | ||
| lastKUpdate = block.timestamp; | ||
| emit SetTargetK(_K); | ||
| } | ||
|
|
||
| function setMaxDolaPrice(uint _maxDolaPrice) external onlyGov { | ||
| require(_maxDolaPrice >= minDolaPrice, "Max DOLA price must be higher or equal to min DOLA price"); | ||
| maxDolaPrice = _maxDolaPrice; | ||
| } | ||
|
|
||
| function setMinDolaPrice(uint _minDolaPrice) external onlyGov { | ||
| require(maxDolaPrice >= _minDolaPrice, "Max DOLA price must be higher or equal to min DOLA price"); | ||
| minDolaPrice = _minDolaPrice; | ||
| } | ||
|
|
||
| function getVirtualDolaReserves() internal view returns(uint) { | ||
| return sqrt(1e18 * getK() / minDolaPrice); | ||
|
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. Can you explain what's going on here? Why is minDolaPrice used here? and how does this square root result in dola reserves? Shouldn't we be dividing K by DBR reserve to get DOLA reserves? |
||
| } | ||
|
|
||
| /** | ||
| * @dev Allows users to buy DBR with DOLA. | ||
| * WARNING: Never expose this directly to a UI as it's likely to cause a loss unless a transaction is executed immediately. | ||
| * Instead use the sDolaHelper function or custom smart contract code. | ||
| * @param exactDolaIn The exact amount of DOLA to spend. | ||
| * @param exactDbrOut The exact amount of DBR to receive. | ||
| * @param to The address that will receive the DBR. | ||
| */ | ||
| function buyDBR(uint exactDolaIn, uint exactDbrOut, address to) external buyHook { | ||
| require(to != address(0), "Zero address"); | ||
| _invariantCheck(exactDolaIn, exactDbrOut, getDbrReserve()); | ||
| dola.transferFrom(msg.sender, address(this), exactDolaIn); | ||
| uint dbrBal = dbr.balanceOf(address(this)); | ||
| if(exactDbrOut > dbrBal) | ||
| dbr.mint(address(this), exactDbrOut - dbrBal); | ||
| uint exactDbrOutAfterFee = exactDbrOut * (1e18 - dbrBuyFee) / 1e18; | ||
| feesAccrued += exactDbrOut - exactDbrOutAfterFee; | ||
|
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. Why mint the fee at all? Instead we should deduct it from the minted amount (or burn it from balance) and remove the feeRecipient role entirely. |
||
| dbr.transfer(to, exactDbrOutAfterFee); | ||
| emit BuyDBR(msg.sender, to, exactDolaIn, exactDbrOut); | ||
| } | ||
|
|
||
| function burnDBR(uint exactDolaIn, uint exactDbrBurn) external buyHook { | ||
| _invariantCheck(exactDolaIn, exactDbrBurn, getDbrReserve()); | ||
| dola.transferFrom(msg.sender, address(this), exactDolaIn); | ||
| dbr.burn(exactDbrBurn); | ||
| emit Burn(msg.sender, exactDolaIn, exactDbrBurn); | ||
| } | ||
|
|
||
| function buyDola(uint exactDbrIn, uint exactDolaOut, address to) external buyHook { | ||
| require(to != address(0), "Zero address"); | ||
| uint k = getK(); | ||
| uint dolaReserve = getDolaReserve(); | ||
| uint dbrReserve = k / dolaReserve; | ||
| _invariantCheck(dbrReserve - exactDbrIn, dolaReserve - exactDolaOut, getDolaReserve()); | ||
| dbr.transferFrom(msg.sender, address(this), exactDolaOut); | ||
| dola.transfer(to, exactDbrIn); | ||
| emit BuyDOLA(msg.sender, to, exactDbrIn, exactDolaOut); | ||
| } | ||
|
|
||
| function _invariantCheck(uint newDolaReserve, uint newDbrReserve, uint k) internal view { | ||
| if(newDolaReserve * newDbrReserve < k){ | ||
|
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. Shouldn't it be |
||
| if(1e18 * newDbrReserve / newDolaReserve > maxDolaPrice){ | ||
| uint maxDolaReserve = sqrt(1e18 * k / maxDolaPrice); | ||
| uint maxDbr = k / maxDolaReserve; | ||
| uint excessDola = newDolaReserve - maxDolaReserve; | ||
| uint excessDbr = newDbrReserve - maxDbr; | ||
| if(1e18 * excessDbr / excessDola >= maxDolaPrice) revert Invariant(); | ||
|
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. Why are we enforcing price invariants when the price is determined by our own AMM? This will essentially halt trading beyond price constraints while the intended behavior would be to continue trading at the price constraints. I think maybe the invariant design might be inappropriate here and instead we could opt for a simple price function that derives price from xy=k up until the constraints then returns fixed prices beyond them. |
||
| } else { | ||
| revert Invariant(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets a new pending governance address. | ||
| * @param _gov The address of the new pending governance. | ||
| */ | ||
| function setPendingGov(address _gov) external onlyGov { | ||
| pendingGov = _gov; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Allows the pending governance to accept its role. | ||
| */ | ||
| function acceptGov() external { | ||
| require(msg.sender == pendingGov, "ONLY PENDINGGOV"); | ||
| gov = pendingGov; | ||
| pendingGov = address(0); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Allows governance to sweep any ERC20 token from the contract. | ||
| * @dev Excludes the ability to sweep DBR tokens. | ||
| * @param token The address of the ERC20 token to sweep. | ||
| * @param amount The amount of tokens to sweep. | ||
| * @param to The recipient address of the swept tokens. | ||
| */ | ||
| function sweep(address token, uint amount, address to) public onlyGov { | ||
| require(address(dbr) != token, "Not authorized"); | ||
| IERC20(token).transfer(to, amount); | ||
| } | ||
|
|
||
| function harvest() public { | ||
|
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. Consider removing the feeRecipient role and removing this function. We can simply burn the excess DBR fees |
||
| uint dbrBalance = dbr.balanceOf(address(this)); | ||
| if(dbrBalance < feesAccrued){ | ||
| dbr.mint(address(this), feesAccrued - dbrBalance); | ||
| } | ||
| dbr.transfer(feeRecipient, feesAccrued); | ||
| feesAccrued = 0; | ||
| } | ||
|
|
||
| function sqrt(uint y) internal pure returns (uint z) { | ||
| if (y > 3) { | ||
| z = y; | ||
| uint x = y / 2 + 1; | ||
| while (x < z) { | ||
| z = x; | ||
| x = (y / x + x) / 2; | ||
| } | ||
| } else if (y != 0) { | ||
| z = 1; | ||
| } | ||
| } | ||
|
|
||
| event BuyDBR(address indexed caller, address indexed to, uint exactDolaIn, uint exactDbrOut); | ||
| event BuyDOLA(address indexed caller, address indexed to, uint exactDbrIn, uint exactDolaOut); | ||
| event Burn(address indexed caller, uint exactDolaIn, uint exactDbrBurn); | ||
| event SetTargetK(uint newTargetK); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| //SPDX-License-Identifier: Unlicensed | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| interface IDebtManager { | ||
| function increaseDebt(address user, uint amount) external; | ||
| function decreaseDebt(address user, uint amount) external returns(uint); | ||
| function debt(address market, address user) external view returns (uint); | ||
| function marketDebt(address market) external view returns(uint); | ||
| } | ||
|
|
||
| interface IDbrAMM { | ||
| function burnDbr(uint exactDolaIn, uint exactDbrBurn) external returns (uint dolaIn); | ||
|
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. Potential case mismatch. It's called |
||
| } | ||
|
|
||
| interface IHelper { | ||
| function dolaNeededForDbr(uint dbrOut) external view returns (uint dolaIn); | ||
| } | ||
|
|
||
| interface IDBR { | ||
| function markets(address) external view returns (bool); | ||
| } | ||
|
|
||
| interface IDOLA { | ||
| function approve(address, uint) external returns (bool); | ||
| function mint(address, uint) external; | ||
| } | ||
|
|
||
|
|
||
| contract VariableDebtManager is IDebtManager { | ||
|
|
||
| IDbrAMM public immutable amm; //TODO: Doesn't necessarily have to be immutable. Can consider making mutable. | ||
| IDBR public immutable dbr; | ||
| IDOLA public immutable dola; | ||
| IHelper public helper; //Should this functionality be a part of the AMM? | ||
|
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. If we dump the invariant model in favor of a simple price function then yes a helper wouldn't be needed |
||
| mapping(address => mapping(address => uint)) public debtShares; | ||
| mapping(address => uint) public marketDebtShares; | ||
| uint public constant MANTISSA = 10 ** 36; | ||
| uint public totalDebt; | ||
| uint public totalDebtShares; | ||
| uint public lastUpdate; | ||
|
|
||
| constructor(address _amm, address _helper, address _dbr, address _dola) { | ||
| lastUpdate = block.timestamp; | ||
| helper = IHelper(_helper); | ||
| amm = IDbrAMM(_amm); | ||
| dbr = IDBR(_dbr); | ||
| dola = IDOLA(_dola); | ||
| } | ||
|
|
||
| modifier onlyMarket() { | ||
| if(!dbr.markets(msg.sender)){ | ||
| revert OnlyMarket(); | ||
| } | ||
| _; | ||
| } | ||
|
|
||
| modifier updateDebt() { | ||
| _burnDbrDeficit(); | ||
| _; | ||
| } | ||
|
|
||
| error OnlyMarket(); | ||
|
|
||
| //Should be called when switching fixed rate debt to variable debt and when borrowing variable debt | ||
| function increaseDebt(address user, uint additionalDebt) external onlyMarket updateDebt { | ||
| address market = msg.sender; | ||
| totalDebt += additionalDebt; | ||
| uint additionalDebtShares; | ||
| if(totalDebt == 0){ | ||
| additionalDebtShares = additionalDebt * MANTISSA; //Minting a high amount of initial debt shares, as debt per share will increase exponentially over the lifetime of the contract | ||
| } else { | ||
| additionalDebtShares = additionalDebt * totalDebtShares / totalDebt; //TODO: Consider rounding up in favour of other users | ||
|
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 can simply add a |
||
| } | ||
| totalDebtShares += additionalDebtShares; | ||
| marketDebtShares[market] += additionalDebtShares; | ||
| debtShares[market][user] += additionalDebtShares; | ||
| } | ||
|
|
||
| //Should be called when switching variable debt with fixed rate debt, when repaying and when a user is liquidated | ||
| function decreaseDebt(address user, uint amount) external onlyMarket updateDebt returns(uint){ | ||
| address market = msg.sender; | ||
| uint userDebt = _debt(market, user); | ||
| if(userDebt <= amount){ | ||
| totalDebtShares -= debtShares[market][user]; | ||
| marketDebtShares[market] -= debtShares[market][user]; | ||
| debtShares[market][user] = 0; | ||
| totalDebt -= userDebt; | ||
| return userDebt; | ||
| } else { | ||
| uint removedDebtShares = totalDebtShares * amount / totalDebt; | ||
| totalDebt -= amount; | ||
| totalDebtShares -= removedDebtShares; //TODO: Make sure this doesn't underflow | ||
|
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. It's impossible to underflow given the if condition above, no? It'll always be less than totalDebtShares |
||
| marketDebtShares[market] -= removedDebtShares; //TODO: Make sure this doesn't underflow | ||
| debtShares[market][user] -= removedDebtShares; //TODO: Make sure this doesn't underflow | ||
| return amount; | ||
| } | ||
| } | ||
|
|
||
| function buyHook() external { | ||
| _burnDbrDeficit(); | ||
| } | ||
|
|
||
| function _burnDbrDeficit() internal { | ||
| if(lastUpdate < block.timestamp){ | ||
| uint _dbrDeficit = dbrDeficit(); | ||
| uint dolaNeeded = helper.dolaNeededForDbr(_dbrDeficit); | ||
| totalDebt += dolaNeeded; | ||
| lastUpdate = block.timestamp; | ||
| dola.mint(address(this), dolaNeeded); | ||
|
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. There's no approval to the AMM, how is it expected to pull the DOLAs from this contract? Also I'm not sure debt manager's should each have DOLA minting rights. The risk seems disproprotionately high. Either we should pre-deposit DOLAs in it or there should be an intermediate middleware than restricts infinite minting. I prefer the first option imo |
||
| amm.burnDbr(dolaNeeded, _dbrDeficit); | ||
|
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. Careful of the interface mismatch. Should be |
||
| } | ||
| } | ||
|
|
||
| function dbrDeficit() public view returns (uint){ | ||
|
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. In the market contract on the #56 branch, the interface of this function has an address borrower param. Seems to mismatch here. |
||
| return (block.timestamp - lastUpdate) * totalDebt / 365 days; | ||
| } | ||
|
|
||
| function debt(address market, address user) public view returns (uint) { | ||
| uint dolaNeeded = helper.dolaNeededForDbr(dbrDeficit()); | ||
| return (totalDebt + dolaNeeded) * debtShares[market][user] / totalDebtShares; | ||
| } | ||
|
|
||
| function marketDebt(address market) public view returns (uint) { | ||
| uint dolaNeeded = helper.dolaNeededForDbr(dbrDeficit()); | ||
| return (totalDebt + dolaNeeded) * marketDebtShares[market] / totalDebtShares; | ||
| } | ||
|
|
||
| //Only safe to use if DBR deficit is 0 | ||
| function _debt(address market, address user) internal view returns (uint){ | ||
| return totalDebt * debtShares[market][user] / totalDebtShares; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DBR reserve is not used here as claimed in the natspec comment. Can you explain how dola reserve should be calculated in the context of this AMM?