Skip to content

Conversation

@0xtj24
Copy link
Collaborator

@0xtj24 0xtj24 commented Jun 8, 2025

No description provided.

src/PSM.sol Outdated
_;
}

function buy(address to, uint256 amount) external {
Copy link

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.

Copy link
Collaborator Author

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;
Copy link

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");
Copy link

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.

Copy link
Collaborator Author

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;
Copy link

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.

Comment on lines +142 to +144
if (vault.balanceOf(address(this)) != 0) {
vault.redeem(vault.balanceOf(address(this)), address(this), address(this));
}
Copy link

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

Copy link
Collaborator Author

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
Comment on lines 81 to 82
collateral.approve(address(vault), amountIn);
vault.deposit(amountIn, address(this));
Copy link

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

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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
Comment on lines 117 to 123
// 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;
Copy link

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:

Suggested change
// 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();

Copy link
Collaborator Author

@0xtj24 0xtj24 Jun 17, 2025

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 {
Copy link

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.

Copy link
Collaborator Author

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;
Copy link

Choose a reason for hiding this comment

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

Gov should be migrateable

Copy link
Collaborator Author

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);
Copy link
Member

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

Copy link
Collaborator Author

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,
Copy link
Member

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

Copy link
Collaborator Author

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 {
Copy link
Member

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

Copy link
Collaborator Author

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 {
Copy link
Member

Choose a reason for hiding this comment

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

collateralAmountIn is clearer

Copy link
Collaborator Author

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) {
Copy link
Member

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

Copy link
Collaborator Author

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

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.

Copy link
Member

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)

* @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) {
Copy link
Member

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6dd04c9

* @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) {
Copy link
Member

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.

Copy link
Collaborator Author

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");
Copy link
Member

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

Copy link
Collaborator Author

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;
Copy link
Member

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

Copy link
Collaborator Author

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) {
Copy link
Member

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");
Copy link
Member

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.

Copy link
Collaborator Author

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) {
Copy link
Member

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4a419d5

@0xtj24 0xtj24 closed this Jul 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants