Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/vault-v2-accrue-nested-entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@morpho-org/blue-sdk": minor
Comment thread
Foulks-Plb marked this conversation as resolved.
"@morpho-org/blue-sdk-viem": patch
"@morpho-org/morpho-sdk": patch
"@morpho-org/wdk-protocol-lending-morpho-evm": patch
---

Accrue every underlying adapter, market, and position when accruing a Vault V2, so `AccrualVaultV2.accrueInterest` returns an entity graph whose entire nested state shares one `lastUpdate` — aligning Vault V2 with the existing MetaMorpho V1 (`AccrualVault.accrueInterest`) behavior, rather than leaving nested adapters at pre-accrual state. Adds an additive `accrueInterest(timestamp?)` method to the `IAccrualVaultV2Adapter` interface and each adapter implementation. Vault-level `_totalAssets`, `totalSupply`, and fee shares are byte-for-byte unchanged.
90 changes: 89 additions & 1 deletion packages/blue-sdk/src/vault/v2/VaultV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ function adapterBaseInput(): Omit<IVaultV2Adapter, "adapterId" | "type"> {
function accrualAdapter(
overrides: Partial<IAccrualVaultV2Adapter> = {},
): IAccrualVaultV2Adapter {
return {
const adapter: IAccrualVaultV2Adapter = {
...vaultV2AdapterInput({ type: "AccrualAdapter" }),
realAssets: () => 1_100n,
accrueInterest: () => adapter,
maxDeposit: (_data, assets) => ({
value: BigInt(assets),
limiter: CapacityLimitReason.balance,
Expand All @@ -62,6 +63,7 @@ function accrualAdapter(
}),
...overrides,
};
return adapter;
}

function accrualVaultV2(
Expand Down Expand Up @@ -323,6 +325,32 @@ describe("AccrualVaultV2.accrueInterest", () => {
expect(result.managementFeeShares).toBe(0n);
expect(result.vault.totalSupply).toBe(1_000n + result.performanceFeeShares);
});

test("behavior: accrues nested adapters and the liquidity adapter to the same timestamp", () => {
const adapter = new AccrualVaultV2MorphoMarketV1Adapter(
{
...adapterBaseInput(),
marketParamsList: [new MarketParams(marketParams())],
},
[accrualPosition({ supplyShares: 100n })],
);
const vault = accrualVaultV2(adapter);

const { vault: accrued } = vault.accrueInterest(101n);

expect(accrued.lastUpdate).toBe(101n);

const accruedAdapter = accrued
.accrualAdapters[0] as AccrualVaultV2MorphoMarketV1Adapter;
const accruedLiquidity =
accrued.accrualLiquidityAdapter as AccrualVaultV2MorphoMarketV1Adapter;
expect(accruedAdapter).not.toBe(adapter);
expect(accruedAdapter.positions[0]?.market.lastUpdate).toBe(101n);
expect(accruedLiquidity.positions[0]?.market.lastUpdate).toBe(101n);

// Accrual is non-mutating: the source adapter keeps its original state.
expect(adapter.positions[0]?.market.lastUpdate).toBe(100n);
});
});

describe("VaultV2Adapter", () => {
Expand Down Expand Up @@ -403,6 +431,21 @@ describe("AccrualVaultV2MorphoMarketV1Adapter", () => {
limiter: CapacityLimitReason.position,
});
});

test("accrueInterest accrues underlying positions and preserves realAssets", () => {
const position = accrualPosition({ supplyShares: 100n });
const adapter = new AccrualVaultV2MorphoMarketV1Adapter(
{ ...adapterBaseInput(), marketParamsList: [position.market.params] },
[position],
);

const accrued = adapter.accrueInterest(101n);

expect(accrued).not.toBe(adapter);
expect(accrued.positions[0]?.market.lastUpdate).toBe(101n);
expect(adapter.positions[0]?.market.lastUpdate).toBe(100n);
expect(accrued.realAssets(101n)).toBe(adapter.realAssets(101n));
});
});

describe("VaultV2MorphoMarketV1AdapterV2", () => {
Expand Down Expand Up @@ -518,6 +561,26 @@ describe("AccrualVaultV2MorphoMarketV1AdapterV2", () => {
limiter: CapacityLimitReason.position,
});
});

test("accrueInterest accrues underlying markets and preserves realAssets", () => {
const m = market();
const adapter = new AccrualVaultV2MorphoMarketV1AdapterV2(
{
...adapterBaseInput(),
marketIds: [m.id],
adaptiveCurveIrm: ADAPTER,
supplyShares: { [m.id]: 100n },
},
[m],
);

const accrued = adapter.accrueInterest(101n);

expect(accrued).not.toBe(adapter);
expect(accrued.markets[0]?.lastUpdate).toBe(101n);
expect(adapter.markets[0]?.lastUpdate).toBe(100n);
expect(accrued.realAssets(101n)).toBe(adapter.realAssets(101n));
});
});

describe("VaultV2MorphoVaultV1Adapter", () => {
Expand Down Expand Up @@ -567,4 +630,29 @@ describe("AccrualVaultV2MorphoVaultV1Adapter", () => {
limiter: CapacityLimitReason.liquidity,
});
});

test("accrueInterest delegates to the underlying V1 vault's accrueInterest", () => {
const accruedVaultV1 = {
toAssets: (shares: bigint) => shares,
} as AccrualVault;
let accruedAt: bigint | undefined;
const accrualVaultV1 = {
accrueInterest: (timestamp?: bigint) => {
accruedAt = timestamp;
return accruedVaultV1;
},
} as AccrualVault;
const adapter = new AccrualVaultV2MorphoVaultV1Adapter(
{ ...adapterBaseInput(), morphoVaultV1: RECIPIENT },
accrualVaultV1,
10n,
);

const accrued = adapter.accrueInterest(5n);

expect(accrued).not.toBe(adapter);
expect(accruedAt).toBe(5n);
expect(accrued.accrualVaultV1).toBe(accruedVaultV1);
expect(accrued.shares).toBe(10n);
});
});
43 changes: 31 additions & 12 deletions packages/blue-sdk/src/vault/v2/VaultV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,21 @@ export class AccrualVaultV2 extends VaultV2 implements IAccrualVaultV2 {
}

/**
* Returns a new vault derived from this vault, whose interest has been accrued up to the given timestamp.
* Performance and management fee shares are zero when the corresponding fee recipient cannot receive vault shares.
* @param timestamp The timestamp at which to accrue interest. Must be greater than or equal to the vault's `lastUpdate`.
* Returns a new vault derived from this vault, whose interest — together with
* that of every adapter, market, and position it holds — has been accrued up to
* the given timestamp, so the entire returned entity graph shares one
* `lastUpdate`.
* Performance and management fee shares are zero when the corresponding fee
* recipient cannot receive vault shares.
* @param timestamp The timestamp at which to accrue interest. Must be greater
* than or equal to the vault's `lastUpdate` and to each underlying market's
* `lastUpdate`.
* @throws {VaultV2Errors.InvalidInterestAccrual} when `timestamp` precedes the
* vault's `lastUpdate`.
* @throws {BlueErrors.InvalidInterestAccrual} when `timestamp` precedes an
* underlying market's `lastUpdate`.
*/
public accrueInterest(timestamp: BigIntish) {
const vault = new AccrualVaultV2(
this,
this.accrualLiquidityAdapter,
this.accrualAdapters,
this.assetBalance,
this.forceDeallocatePenalties,
);

// biome-ignore lint/style/noParameterAssign: TODO refactor to avoid mutating parameter
timestamp = BigInt(timestamp);

Expand All @@ -257,6 +259,23 @@ export class AccrualVaultV2 extends VaultV2 implements IAccrualVaultV2 {
this.lastUpdate,
);

// Accrue every nested adapter (and the liquidity adapter) to the same
// timestamp, so the returned vault exposes an entity graph that shares one
// `lastUpdate` rather than pre-accrual market state.
const accrualAdapters = this.accrualAdapters.map((adapter) =>
adapter.accrueInterest(timestamp),
);
const accrualLiquidityAdapter =
this.accrualLiquidityAdapter?.accrueInterest(timestamp);

const vault = new AccrualVaultV2(
this,
accrualLiquidityAdapter,
accrualAdapters,
this.assetBalance,
this.forceDeallocatePenalties,
);

// Corresponds to the `firstTotalAssets == 0` onchain check.
if (elapsed === 0n)
return { vault, performanceFeeShares: 0n, managementFeeShares: 0n };
Expand Down Expand Up @@ -300,7 +319,7 @@ export class AccrualVaultV2 extends VaultV2 implements IAccrualVaultV2 {
vault._totalAssets = newTotalAssets;
if (performanceFeeShares) vault.totalSupply += performanceFeeShares;
if (managementFeeShares) vault.totalSupply += managementFeeShares;
vault.lastUpdate = BigInt(timestamp);
vault.lastUpdate = timestamp;

return { vault, performanceFeeShares, managementFeeShares };
}
Expand Down
10 changes: 10 additions & 0 deletions packages/blue-sdk/src/vault/v2/VaultV2Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ export abstract class VaultV2Adapter implements IVaultV2Adapter {
export interface IAccrualVaultV2Adapter extends IVaultV2Adapter {
realAssets(timestamp: BigIntish): bigint;

/**
* Returns a new adapter derived from this one, whose underlying market state
* has been accrued up to the given timestamp. Lets a fully-accrued vault expose
* an entity graph in which every adapter, market, and position shares one
* `lastUpdate` instead of pre-accrual state.
* @param timestamp The timestamp at which to accrue interest. Must be greater
* than or equal to each underlying market's `lastUpdate`.
*/
accrueInterest(timestamp?: BigIntish): IAccrualVaultV2Adapter;
Comment thread
Foulks-Plb marked this conversation as resolved.
Outdated
Comment thread
Foulks-Plb marked this conversation as resolved.
Outdated

/**
* Returns the maximum amount of assets that can be deposited to this adapter.
* @param assets The maximum amount of assets to deposit.
Expand Down
13 changes: 13 additions & 0 deletions packages/blue-sdk/src/vault/v2/VaultV2MorphoMarketV1Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ export class AccrualVaultV2MorphoMarketV1Adapter
);
}

/**
* Returns a new adapter whose underlying positions (and their markets) have
* been accrued up to the given timestamp.
* @param timestamp The timestamp at which to accrue interest. Must be greater
* than or equal to each position's market `lastUpdate`.
*/
accrueInterest(timestamp?: BigIntish) {
return new AccrualVaultV2MorphoMarketV1Adapter(
this,
this.positions.map((position) => position.accrueInterest(timestamp)),
);
}

maxDeposit(_data: Hex, assets: BigIntish) {
return {
value: BigInt(assets),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ export class AccrualVaultV2MorphoMarketV1AdapterV2
);
}

/**
* Returns a new adapter whose underlying markets have been accrued up to the
* given timestamp.
* @param timestamp The timestamp at which to accrue interest. Must be greater
* than or equal to each market's `lastUpdate`.
*/
accrueInterest(timestamp?: BigIntish) {
return new AccrualVaultV2MorphoMarketV1AdapterV2(
this,
this.markets.map((market) => market.accrueInterest(timestamp)),
);
}

maxDeposit(_data: Hex, assets: BigIntish) {
return {
value: BigInt(assets),
Expand Down
14 changes: 14 additions & 0 deletions packages/blue-sdk/src/vault/v2/VaultV2MorphoVaultV1Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ export class AccrualVaultV2MorphoVaultV1Adapter
return this.accrualVaultV1.accrueInterest(timestamp).toAssets(this.shares);
}

/**
* Returns a new adapter whose underlying MetaMorpho V1 vault (and its market
* positions) has been accrued up to the given timestamp.
* @param timestamp The timestamp at which to accrue interest. Must be greater
* than or equal to each underlying market's `lastUpdate`.
*/
accrueInterest(timestamp?: BigIntish) {
return new AccrualVaultV2MorphoVaultV1Adapter(
this,
this.accrualVaultV1.accrueInterest(timestamp),
this.shares,
);
}

maxDeposit(_data: Hex, assets: BigIntish) {
return this.accrualVaultV1.maxDeposit(assets);
}
Expand Down