Skip to content

Commit 990f01a

Browse files
committed
invariant_depositRedeemNoValueExtraction
1 parent 3801a2d commit 990f01a

File tree

3 files changed

+47
-20
lines changed

3 files changed

+47
-20
lines changed

foundry.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ single_line_imports = false
3232

3333
[invariant]
3434
runs = 256
35-
depth = 50
36-
fail_on_revert = false
35+
depth = 100
3736

3837
[lint]
39-
lint_on_build = false
38+
lint_on_build = false

test-foundry/libraries/vault/invariant/MasterVaultHandler.sol

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ contract MasterVaultHandler is Test {
2222

2323
// --- Ghost variables ---
2424

25+
uint256 public random;
26+
2527
/// @notice Cumulative assets deposited into the MasterVault
2628
uint256 public ghost_deposited;
2729

@@ -46,6 +48,11 @@ contract MasterVaultHandler is Test {
4648
/// @notice Whether subvault has been negatively manipulated (loss-like: removed assets, inflated shares, rounding errors)
4749
bool public ghost_negativeManipulation;
4850

51+
modifier updateRandom(uint256 val) {
52+
random = uint256(keccak256(abi.encodePacked(random, val)));
53+
_;
54+
}
55+
4956
constructor(
5057
MasterVault _vault,
5158
FuzzSubVault _subVault,
@@ -64,7 +71,7 @@ contract MasterVaultHandler is Test {
6471

6572
/// @notice Deposit assets into the MasterVault via the gateway user
6673
/// @dev Bounds amount to [1, 1e30] to stay within reasonable range
67-
function deposit(uint256 amount) external {
74+
function deposit(uint256 amount) external updateRandom(uint256(keccak256("deposit"))) updateRandom(amount) {
6875
amount = bound(amount, 1, 1e30);
6976

7077
vm.prank(user);
@@ -81,7 +88,7 @@ contract MasterVaultHandler is Test {
8188

8289
/// @notice Redeem shares from the MasterVault as user
8390
/// @dev Bounds shares to [1, user balance]
84-
function redeem(uint256 shares) external {
91+
function redeem(uint256 shares) external updateRandom(uint256(keccak256("redeem"))) updateRandom(shares) {
8592
uint256 bal = vault.balanceOf(user);
8693
if (bal == 0) return;
8794
shares = bound(shares, 1, bal);
@@ -99,7 +106,7 @@ contract MasterVaultHandler is Test {
99106

100107
/// @notice Warp time and call rebalance with permissive slippage
101108
/// @dev Uses extreme slippage bounds to avoid exchange rate reverts masking real bugs
102-
function rebalance() external {
109+
function rebalance() external updateRandom(uint256(keccak256("rebalance"))) {
103110
vm.warp(block.timestamp + 2);
104111

105112
int256 minExchRate;
@@ -122,7 +129,7 @@ contract MasterVaultHandler is Test {
122129
}
123130

124131
/// @notice Distribute performance fees to beneficiary
125-
function distributePerformanceFee() external {
132+
function distributePerformanceFee() external updateRandom(uint256(keccak256("distributePerformanceFee"))) {
126133
address beneficiary = vault.beneficiary();
127134
uint256 before = token.balanceOf(beneficiary);
128135

@@ -137,7 +144,7 @@ contract MasterVaultHandler is Test {
137144
// --- Manager actions ---
138145

139146
/// @notice Set target allocation (bounded 0 to 1e18)
140-
function setTargetAllocation(uint256 seed) external {
147+
function setTargetAllocation(uint256 seed) external updateRandom(uint256(keccak256("setTargetAllocation"))) updateRandom(seed) {
141148
uint64 alloc = uint64(bound(seed, 0, 1e18));
142149
try vault.setTargetAllocationWad(alloc) {
143150
ghost_callCount[this.setTargetAllocation.selector]++;
@@ -147,7 +154,7 @@ contract MasterVaultHandler is Test {
147154
// --- Environment manipulation ---
148155

149156
/// @notice Send tokens directly to the subvault (simulates yield / A increases)
150-
function simulateSubVaultProfit(uint256 amt) external {
157+
function simulateSubVaultProfit(uint256 amt) external updateRandom(uint256(keccak256("simulateSubVaultProfit"))) updateRandom(amt) {
151158
amt = bound(amt, 1, 1e24);
152159
token.mintAmount(amt);
153160
token.transfer(address(subVault), amt);
@@ -157,7 +164,7 @@ contract MasterVaultHandler is Test {
157164
}
158165

159166
/// @notice Remove tokens from the subvault (simulates loss / A decreases)
160-
function simulateSubVaultLoss(uint256 amt) external {
167+
function simulateSubVaultLoss(uint256 amt) external updateRandom(uint256(keccak256("simulateSubVaultLoss"))) updateRandom(amt) {
161168
uint256 subBal = token.balanceOf(address(subVault));
162169
if (subBal == 0) return;
163170
amt = bound(amt, 1, subBal);
@@ -170,15 +177,15 @@ contract MasterVaultHandler is Test {
170177
}
171178

172179
/// @notice Mint shares without backing assets on FuzzSubVault (T increases, A < T)
173-
function inflateSubVaultShares(uint256 amt) external {
180+
function inflateSubVaultShares(uint256 amt) external updateRandom(uint256(keccak256("inflateSubVaultShares"))) updateRandom(amt) {
174181
amt = bound(amt, 1, 1e24);
175182
subVault.adminMint(address(vault), amt);
176183
ghost_negativeManipulation = true;
177184
ghost_callCount[this.inflateSubVaultShares.selector]++;
178185
}
179186

180187
/// @notice Burn shares without withdrawing assets on FuzzSubVault (T decreases, A > T)
181-
function deflateSubVaultShares(uint256 amt) external {
188+
function deflateSubVaultShares(uint256 amt) external updateRandom(uint256(keccak256("deflateSubVaultShares"))) updateRandom(amt) {
182189
uint256 vaultShares = subVault.balanceOf(address(vault));
183190
if (vaultShares == 0) return;
184191
amt = bound(amt, 1, vaultShares);
@@ -188,60 +195,60 @@ contract MasterVaultHandler is Test {
188195
}
189196

190197
/// @notice Set maxWithdraw limit on the subvault
191-
function capSubVaultMaxWithdraw(uint256 lim) external {
198+
function capSubVaultMaxWithdraw(uint256 lim) external updateRandom(uint256(keccak256("capSubVaultMaxWithdraw"))) updateRandom(lim) {
192199
lim = bound(lim, 0, type(uint128).max);
193200
subVault.setMaxWithdrawLimit(lim);
194201
ghost_callCount[this.capSubVaultMaxWithdraw.selector]++;
195202
}
196203

197204
/// @notice Set maxDeposit limit on the subvault
198-
function capSubVaultMaxDeposit(uint256 lim) external {
205+
function capSubVaultMaxDeposit(uint256 lim) external updateRandom(uint256(keccak256("capSubVaultMaxDeposit"))) updateRandom(lim) {
199206
lim = bound(lim, 0, type(uint128).max);
200207
subVault.setMaxDepositLimit(lim);
201208
ghost_callCount[this.capSubVaultMaxDeposit.selector]++;
202209
}
203210

204211
/// @notice Set maxRedeem limit on the subvault
205-
function capSubVaultMaxRedeem(uint256 lim) external {
212+
function capSubVaultMaxRedeem(uint256 lim) external updateRandom(uint256(keccak256("capSubVaultMaxRedeem"))) updateRandom(lim) {
206213
lim = bound(lim, 0, type(uint128).max);
207214
subVault.setMaxRedeemLimit(lim);
208215
ghost_callCount[this.capSubVaultMaxRedeem.selector]++;
209216
}
210217

211218
/// @notice Set deposit rounding error on the subvault (0–10% in wad)
212-
function setDepositError(uint256 seed) external {
219+
function setDepositError(uint256 seed) external updateRandom(uint256(keccak256("setDepositError"))) updateRandom(seed) {
213220
uint256 wad = bound(seed, 0, 1e17);
214221
subVault.setDepositErrorWad(wad);
215222
if (wad > 0) ghost_negativeManipulation = true;
216223
ghost_callCount[this.setDepositError.selector]++;
217224
}
218225

219226
/// @notice Set withdraw rounding error on the subvault (0–10% in wad)
220-
function setWithdrawError(uint256 seed) external {
227+
function setWithdrawError(uint256 seed) external updateRandom(uint256(keccak256("setWithdrawError"))) updateRandom(seed) {
221228
uint256 wad = bound(seed, 0, 1e17);
222229
subVault.setWithdrawErrorWad(wad);
223230
if (wad > 0) ghost_negativeManipulation = true;
224231
ghost_callCount[this.setWithdrawError.selector]++;
225232
}
226233

227234
/// @notice Set redeem rounding error on the subvault (0–10% in wad)
228-
function setRedeemError(uint256 seed) external {
235+
function setRedeemError(uint256 seed) external updateRandom(uint256(keccak256("setRedeemError"))) updateRandom(seed) {
229236
uint256 wad = bound(seed, 0, 1e17);
230237
subVault.setRedeemErrorWad(wad);
231238
if (wad > 0) ghost_negativeManipulation = true;
232239
ghost_callCount[this.setRedeemError.selector]++;
233240
}
234241

235242
/// @notice Set previewMint rounding error on the subvault (0–10% in wad)
236-
function setPreviewMintError(uint256 seed) external {
243+
function setPreviewMintError(uint256 seed) external updateRandom(uint256(keccak256("setPreviewMintError"))) updateRandom(seed) {
237244
uint256 wad = bound(seed, 0, 1e17);
238245
subVault.setPreviewMintErrorWad(wad);
239246
if (wad > 0) ghost_negativeManipulation = true;
240247
ghost_callCount[this.setPreviewMintError.selector]++;
241248
}
242249

243250
/// @notice Set previewRedeem rounding error on the subvault (0–10% in wad)
244-
function setPreviewRedeemError(uint256 seed) external {
251+
function setPreviewRedeemError(uint256 seed) external updateRandom(uint256(keccak256("setPreviewRedeemError"))) updateRandom(seed) {
245252
uint256 wad = bound(seed, 0, 1e17);
246253
subVault.setPreviewRedeemErrorWad(wad);
247254
if (wad > 0) ghost_negativeManipulation = true;

test-foundry/libraries/vault/invariant/MasterVaultInvariant.t.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,27 @@ contract MasterVaultInvariant is Test {
158158
);
159159
}
160160

161+
/// @notice A deposit-redeem round-trip must never extract value.
162+
/// @dev At any reachable state (arbitrary exchange rates from handler actions),
163+
/// depositing X and immediately redeeming should return <= X.
164+
/// Catches: share pricing rounding that favors depositor over vault.
165+
function invariant_depositRedeemNoValueExtraction() public {
166+
uint256 depositAmount = bound(handler.random(), 1, 1e18);
167+
vm.prank(user);
168+
token.mintAmount(depositAmount);
169+
vm.startPrank(user);
170+
token.approve(address(vault), depositAmount);
171+
uint256 shares = vault.deposit(depositAmount);
172+
vm.stopPrank();
173+
174+
uint256 balBefore = token.balanceOf(user);
175+
vm.prank(user);
176+
vault.redeem(shares, 0);
177+
uint256 assetsReceived = token.balanceOf(user) - balBefore;
178+
179+
assertLe(assetsReceived, depositAmount, "deposit-redeem round-trip extracted value");
180+
}
181+
161182
function _rebalanceToZero() internal returns (bool skip) {
162183
if (vault.targetAllocationWad() != 0) {
163184
vault.setTargetAllocationWad(0);

0 commit comments

Comments
 (0)