-
Notifications
You must be signed in to change notification settings - Fork 0
PSM #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
Conversation
src/PSM.sol
Outdated
| _; | ||
| } | ||
|
|
||
| function buy(address to, uint256 amount) external { |
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.
May want to check maxDeposit for the ERC4626, though I don't think it's strictly necessary.
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.
Imo it's not necessary, it would still revert on the vault if there's a limit (sUSDS has max uint256), if we avoid it we could save same gas (same for checking max withdraw). But can add it if you think it's better to have our custom revert.
src/PSM.sol
Outdated
| uint256 amountIn = amount; | ||
| if (depositFeeBps > 0) { | ||
| uint256 fee = (amount * depositFeeBps) / BPS_DENOMINATOR; | ||
| amountIn += fee; |
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.
Nitpick: I prefer subtracting fees from DOLA paid out to the buyer, than adding the fee to the amountIn, but it's not a hill I'm going to die on.
src/PSM.sol
Outdated
| } | ||
|
|
||
| function sweep(IERC20 token) public onlyOperator { | ||
| require(token != IERC20(address(vault)), "Vault token cannot be swept"); |
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.
Vault token should be sweepable for emergencies.
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.
Fixed in f6de449
src/PSM.sol
Outdated
| using SafeERC20 for IERC20; | ||
|
|
||
| IERC20 public immutable collateral; | ||
| IERC4626 public immutable vault; |
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.
Vaults should be setable and migrateable, so the PSM can be used to dogfood lending markets in the future.
| if (vault.balanceOf(address(this)) != 0) { | ||
| vault.redeem(vault.balanceOf(address(this)), address(this), address(this)); | ||
| } |
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.
Migration will fail if redeem > maxWithdraw
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.
If maxRedeem exceeds the balance in the PSM, should we allow to set a specific amount lower than that and allow for multiple smaller migrations?
In this edge case I guess we would then handle both vaults in terms of selling, total reserves and profit or we could just sweep the old vault tokens and redeem them via governance in multiple steps depending on availability (sending shares to the PSM)? Let me think a bit on what could be the side effects if any
src/PSM.sol
Outdated
| collateral.approve(address(vault), amountIn); | ||
| vault.deposit(amountIn, address(this)); |
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.
This pattern is a good example of why we shouldn't add fee to amountIn but rather subtract it from amount out.
Imagine a situation where we suddenly break integrations by increasing fee from 0 to 10 bps, because all approvals are now 10 bps short of the necessary amountIn
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.
My understanding of the PSM was that integrators that need DOLA would use it to get the exact amount out of DOLA rather than a fixed amount in of collateral to get some amount of DOLA depending on fees.
The previous stabilizer only allowed to buy a specific amount of DOLA so kept the same pattern.
Anyway in both patterns if the deposit fee is changed it would require the integrator to use the view functions to adjust the approval of the collateral amount on the PSM to get the DOLA out that they require.
But if I misunderstood and the exact DOLA out is not what they need, can easily update that.
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.
Fixed in d0be880
src/PSM.sol
Outdated
| // Include profit and fees in total reserves | ||
| function getTotalReserves() public view returns (uint256) { | ||
| return vault.previewRedeem(vault.balanceOf(address(this))); | ||
| } | ||
|
|
||
| function getProfit() external view returns (uint256) { | ||
| return getTotalReserves() - supply; |
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.
Consider getting rid of the supply variable by doing the following:
| // Include profit and fees in total reserves | |
| function getTotalReserves() public view returns (uint256) { | |
| return vault.previewRedeem(vault.balanceOf(address(this))); | |
| } | |
| function getProfit() external view returns (uint256) { | |
| return getTotalReserves() - supply; | |
| // Include profit and fees in total reserves | |
| function getTotalReserves() public view returns (uint256) { | |
| return vault.previewRedeem(vault.balanceOf(address(this))) + dola.balanceOf(address(this)); | |
| } | |
| function getProfit() external view returns (uint256) { | |
| return getTotalReserves() + dola.balanceOf(address(this)) - PSMFed.supply(); |
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.
After trying to remove supply, realized that in case of a donation of DOLA to the PSM it would break the profit logic and we would have to sweep the donated DOLA in the best case scenario.
But in the case when takeProfit is called before we sweep, it will withdraw more USDS than the actual profit so we would end up having to first sweep the DOLA donated and then redeposit into the vault the excess USDS mistakenly taken as profit and send shares to the PSM.
It's more a griefing vector but could be annoying and repeatable.
Is there a specific reason to remove the supply ? Otherwise I would keep it
src/PSM.sol
Outdated
| function setSupplyCap(uint256 newSupplyCap) external onlyOperator { | ||
| supplyCap = newSupplyCap; | ||
| emit SupplyCapUpdated(supplyCap); | ||
| function setGov(address newGov) external onlyGov { |
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.
Should use set pending -> accept pattern for gov change.
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.
Fixed in 0ddf1dc
src/PSMFed.sol
Outdated
|
|
||
| contract PSMFed { | ||
| address public immutable psm; | ||
| address public immutable gov; |
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.
Gov should be migrateable
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.
Fixed in 0ddf1dc
src/PSM.sol
Outdated
| import {PSMFed} from "src/PSMFed.sol"; | ||
|
|
||
| interface IController { | ||
| function isBuyAllowed(uint256 amount) external view returns (bool); |
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.
should probably pass msg.sender to the controller also in case some rules can be added about the caller in the future
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.
Fixed in 6dd04c9
src/PSM.sol
Outdated
| address _vault, | ||
| address _DOLA, | ||
| address _gov, | ||
| uint256 _depositFeeBps, |
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.
To simplify, fees can be removed from the constructor in which case they will default to zero. In the governance proposal, governance can set the fees anyway
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.
Fixed in 6dd04c9
src/PSM.sol
Outdated
| * @param to Address to receive the collateral. | ||
| * @param amount Amount of DOLA to sell. | ||
| */ | ||
| function sell(address to, uint256 amount) public { |
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.
I think dolaAmountIn is a clearer param here
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.
Fixed in 6dd04c9
src/PSM.sol
Outdated
| * @param to DOLA receiver. | ||
| * @param amountIn Amount of collateral to sell for DOLA. | ||
| */ | ||
| function buy(address to, uint256 amountIn) public { |
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.
collateralAmountIn is clearer
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.
Fixed in 6dd04c9
src/PSM.sol
Outdated
| address oldVault = address(vault); | ||
| vault = IERC4626(newVault); | ||
| uint256 collateralBalance = collateral.balanceOf(address(this)); | ||
| if (collateralBalance != 0) { |
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.
Actually, I would prefer to revert if collateralBalance is 0 in this step. Something isn't right if we're migrating but nothing ends up going into the new vault
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.
The balance checks were introduced to be able to also call migrate when the migration has to be done manually via gov (in the unlikely case when there might be redemption or deposit limits in the vaults, fyi sUSDS has no limits).
See test here
Line 256 in 6dd04c9
| function test_migrate_manually_via_gov_if_maxRedeem_lower_than_psm_balance() public { |
It was done to avoid having another function that just
takeProfit and update the vault.But if you think it’s better to have a separate function I can easily update that.
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.
In this case, we should add a minCollateralMigrated param for the caller (gov)
src/Controller.sol
Outdated
| * @return bool Returns true if the buy operation is allowed, false otherwise. | ||
| * For now, it returns true to allow all buy operations. | ||
| */ | ||
| function isBuyAllowed(uint256 amount) external pure returns (bool) { |
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.
Make sure to update this contract if you change the interface in the other file as I suggested
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.
Fixed in 6dd04c9
src/Controller.sol
Outdated
| * @return bool Returns true if the buy operation is allowed, false otherwise. | ||
| * For now, it returns true to allow all buy operations. | ||
| */ | ||
| function isBuyAllowed(uint256 amount) external pure returns (bool) { |
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.
Controller functions are better off being mutable functions (not pure or view) because we might want to store state in this contract e.g. in the case of daily in/outflow limits etc. In this case, we should probably also rename the functions to onBuy and onSell since they become hooks.
Also please lets make sure we're consistent in naming. In some places, we seem to be using sell and withdraw, buy and deposit interchangeably. Lets pick one. I think buy and sell are clearer.
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.
Fixed in 6dd04c9
src/PSMFed.sol
Outdated
| * @param newChair Address of the new chair. | ||
| */ | ||
| function setChair(address newChair) external onlyGov { | ||
| require(newChair != address(0), "Invalid address"); |
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.
Maybe it should be 0 (no chair). Either way, no risk here in cases of mistakes so checks aren't necessary
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.
Fixed in 6dd04c9
src/PSM.sol
Outdated
| uint256 amountOut = collateralAmountIn; | ||
|
|
||
| if (depositFeeBps > 0) { | ||
| uint256 fee = (collateralAmountIn * depositFeeBps) / BPS_DENOMINATOR; |
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.
We're still using depositFee and withdrawalFee naming here and in other places when the function is called buy. Let's rename it to buyFee and sellFee for consistency
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.
Fixed in 08ddd69 and following
src/PSM.sol
Outdated
| address oldVault = address(vault); | ||
| vault = IERC4626(newVault); | ||
| uint256 collateralBalance = collateral.balanceOf(address(this)); | ||
| if (collateralBalance != 0) { |
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.
In this case, we should add a minCollateralMigrated param for the caller (gov)
src/PSM.sol
Outdated
| */ | ||
| function sell(address to, uint256 dolaAmountIn) public { | ||
| require(dolaAmountIn > 0, "Amount must be > 0"); | ||
| require(controller.onBuy(msg.sender, dolaAmountIn), "Denied by controller"); |
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.
using onBuy instead of onSell here. Caught by AI.
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.
Fixed in 485612f
src/PSM.sol
Outdated
| address oldVault = address(vault); | ||
| vault = IERC4626(newVault); | ||
| uint256 collateralBalance = collateral.balanceOf(address(this)); | ||
| if (collateralBalance >= minCollateralAmount) { |
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.
The alternative needs to be a revert. So in this case we need to switch this to a require instead of an if statement
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.
Fixed in 4a419d5
No description provided.