Skip to content

Commit 92134e2

Browse files
authored
feat: enforce spoke refresh premium is contained (#709)
1 parent 0da358d commit 92134e2

File tree

4 files changed

+108
-45
lines changed

4 files changed

+108
-45
lines changed

snapshots/Hub.Operations.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"add": "119571",
3-
"draw": "112318",
4-
"refreshPremium": "81862",
3+
"draw": "112344",
4+
"refreshPremium": "84297",
55
"remove: full": "80336",
66
"remove: partial": "86836",
7-
"restore: full": "111592",
8-
"restore: partial": "117444"
7+
"restore: full": "114054",
8+
"restore: partial": "119906"
99
}

snapshots/Spoke.Operations.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"borrow: first": "248044",
3-
"borrow: second action, same reserve": "211516",
4-
"liquidationCall: full": "316449",
5-
"liquidationCall: partial": "340120",
6-
"permitReserve + repay (multicall)": "268464",
2+
"borrow: first": "250505",
3+
"borrow: second action, same reserve": "213977",
4+
"liquidationCall: full": "321346",
5+
"liquidationCall: partial": "345017",
6+
"permitReserve + repay (multicall)": "273361",
77
"permitReserve + supply (multicall)": "140631",
88
"permitReserve + supply + enable collateral (multicall)": "174116",
9-
"repay: full": "172756",
10-
"repay: partial": "227225",
9+
"repay: full": "175218",
10+
"repay: partial": "232122",
1111
"setUserPositionManagerWithSig: disable": "38977",
1212
"setUserPositionManagerWithSig: enable": "63000",
1313
"supply + enable collateral (multicall)": "152550",
@@ -17,12 +17,12 @@
1717
"supply: second action, same reserve": "102491",
1818
"updateUserDynamicConfig: 1 collateral": "53152",
1919
"updateUserDynamicConfig: 2 collaterals": "58433",
20-
"updateUserRiskPremium: 1 borrow": "121382",
21-
"updateUserRiskPremium: 2 borrows": "157262",
20+
"updateUserRiskPremium: 1 borrow": "123817",
21+
"updateUserRiskPremium: 2 borrows": "162132",
2222
"usingAsCollateral: 0 borrows, enable": "54026",
23-
"usingAsCollateral: 1 borrow, disable": "131608",
23+
"usingAsCollateral: 1 borrow, disable": "134043",
2424
"usingAsCollateral: 1 borrow, enable": "24786",
2525
"withdraw: 0 borrows, full": "135168",
2626
"withdraw: 0 borrows, partial": "137271",
27-
"withdraw: 1 borrow, partial": "237956"
27+
"withdraw: 1 borrow, partial": "240391"
2828
}

src/contracts/Hub.sol

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ contract Hub is IHub, AccessManaged {
263263
uint128 drawnShares = previewRestoreByAssets(assetId, drawnAmount).toUint128();
264264
asset.drawnShares -= drawnShares;
265265
spoke.drawnShares -= drawnShares;
266-
_applyPremiumDelta(asset, spoke, premiumDelta, premiumAmount);
266+
_applyPremiumDelta(assetId, asset, spoke, premiumDelta, premiumAmount);
267267
uint256 totalAmount = drawnAmount + premiumAmount;
268268
asset.liquidity += totalAmount.toUint128();
269269

@@ -293,7 +293,7 @@ contract Hub is IHub, AccessManaged {
293293
uint128 drawnShares = previewRestoreByAssets(assetId, drawnAmount).toUint128();
294294
asset.drawnShares -= drawnShares;
295295
spoke.drawnShares -= drawnShares;
296-
_applyPremiumDelta(asset, spoke, premiumDelta, premiumAmount);
296+
_applyPremiumDelta(assetId, asset, spoke, premiumDelta, premiumAmount);
297297
uint256 totalDeficitAmount = drawnAmount + premiumAmount;
298298
asset.deficit += totalDeficitAmount.toUint128();
299299

@@ -335,7 +335,7 @@ contract Hub is IHub, AccessManaged {
335335
asset.accrue(assetId, _spokes[assetId][asset.feeReceiver]);
336336

337337
// no premium change allowed
338-
_applyPremiumDelta(asset, spoke, premiumDelta, 0);
338+
_applyPremiumDelta(assetId, asset, spoke, premiumDelta, 0);
339339

340340
emit RefreshPremium(assetId, msg.sender, premiumDelta);
341341
}
@@ -522,12 +522,13 @@ contract Hub is IHub, AccessManaged {
522522
}
523523

524524
function getSpokeOwed(uint256 assetId, address spoke) external view returns (uint256, uint256) {
525-
return _getSpokeOwed(_spokes[assetId][spoke], assetId);
525+
DataTypes.SpokeData storage spokeData = _spokes[assetId][spoke];
526+
return (_getSpokeDrawn(spokeData, assetId), _getSpokePremium(spokeData, assetId));
526527
}
527528

528529
function getSpokeTotalOwed(uint256 assetId, address spoke) external view returns (uint256) {
529-
(uint256 drawn, uint256 premium) = _getSpokeOwed(_spokes[assetId][spoke], assetId);
530-
return drawn + premium;
530+
DataTypes.SpokeData storage spokeData = _spokes[assetId][spoke];
531+
return _getSpokeDrawn(spokeData, assetId) + _getSpokePremium(spokeData, assetId);
531532
}
532533

533534
function getAssetAddedAmount(uint256 assetId) external view returns (uint256) {
@@ -603,15 +604,17 @@ contract Hub is IHub, AccessManaged {
603604

604605
/**
605606
* @dev Applies premium deltas on asset and spoke owed, and validates that total premium
606-
* cannot decrease by more than `premiumAmount`.
607+
* and spoke premium cannot decrease by more than `premiumAmount`.
607608
*/
608609
function _applyPremiumDelta(
610+
uint256 assetId,
609611
DataTypes.Asset storage asset,
610612
DataTypes.SpokeData storage spoke,
611613
DataTypes.PremiumDelta calldata premium,
612614
uint256 premiumAmount
613615
) internal {
614-
uint256 premiumBefore = asset.premium();
616+
uint256 assetPremiumBefore = asset.premium();
617+
uint256 spokePremiumBefore = _getSpokePremium(spoke, assetId);
615618

616619
asset.premiumShares = asset.premiumShares.add(premium.sharesDelta).toUint128();
617620
asset.premiumOffset = asset.premiumOffset.add(premium.offsetDelta).toUint128();
@@ -622,7 +625,9 @@ contract Hub is IHub, AccessManaged {
622625
spoke.realizedPremium = spoke.realizedPremium.add(premium.realizedDelta).toUint128();
623626

624627
// can increase due to precision loss on premium (drawn unchanged)
625-
require(asset.premium() + premiumAmount - premiumBefore <= 2, InvalidPremiumChange());
628+
require(asset.premium() + premiumAmount - assetPremiumBefore <= 2, InvalidPremiumChange());
629+
uint256 spokePremiumAfter = _getSpokePremium(spoke, assetId);
630+
require(spokePremiumAfter + premiumAmount - spokePremiumBefore <= 2, InvalidPremiumChange());
626631
}
627632

628633
function _transferShares(
@@ -637,16 +642,20 @@ contract Hub is IHub, AccessManaged {
637642
receiver.addedShares += shares.toUint128();
638643
}
639644

640-
function _getSpokeOwed(
645+
function _getSpokeDrawn(
646+
DataTypes.SpokeData storage spoke,
647+
uint256 assetId
648+
) internal view returns (uint256) {
649+
return previewRestoreByShares(assetId, spoke.drawnShares);
650+
}
651+
652+
function _getSpokePremium(
641653
DataTypes.SpokeData storage spoke,
642654
uint256 assetId
643-
) internal view returns (uint256, uint256) {
655+
) internal view returns (uint256) {
644656
uint256 accruedPremium = previewRestoreByShares(assetId, spoke.premiumShares) -
645657
spoke.premiumOffset;
646-
return (
647-
previewRestoreByShares(assetId, spoke.drawnShares),
648-
spoke.realizedPremium + accruedPremium
649-
);
658+
return spoke.realizedPremium + accruedPremium;
650659
}
651660

652661
function _validateAdd(
@@ -691,7 +700,8 @@ contract Hub is IHub, AccessManaged {
691700
require(amount > 0, InvalidAmount());
692701
require(spoke.active, SpokeNotActive());
693702
uint256 drawCap = spoke.drawCap;
694-
(uint256 drawn, uint256 premium) = _getSpokeOwed(spoke, assetId);
703+
uint256 drawn = _getSpokeDrawn(spoke, assetId);
704+
uint256 premium = _getSpokePremium(spoke, assetId);
695705
require(
696706
drawCap == Constants.MAX_CAP || drawCap * 10 ** asset.decimals >= drawn + premium + amount,
697707
DrawCapExceeded(drawCap)
@@ -708,7 +718,8 @@ contract Hub is IHub, AccessManaged {
708718
require(from != address(this), InvalidAddress());
709719
require(drawnAmount + premiumAmount > 0, InvalidAmount());
710720
require(spoke.active, SpokeNotActive());
711-
(uint256 drawn, uint256 premium) = _getSpokeOwed(spoke, assetId);
721+
uint256 drawn = _getSpokeDrawn(spoke, assetId);
722+
uint256 premium = _getSpokePremium(spoke, assetId);
712723
require(drawnAmount <= drawn, SurplusAmountRestored(drawn));
713724
require(premiumAmount <= premium, SurplusAmountRestored(premium));
714725
}
@@ -721,7 +732,8 @@ contract Hub is IHub, AccessManaged {
721732
) internal view {
722733
require(spoke.active, SpokeNotActive());
723734
require(drawnAmount + premiumAmount > 0, InvalidAmount());
724-
(uint256 drawn, uint256 premium) = _getSpokeOwed(spoke, assetId);
735+
uint256 drawn = _getSpokeDrawn(spoke, assetId);
736+
uint256 premium = _getSpokePremium(spoke, assetId);
725737
require(drawnAmount <= drawn, SurplusDeficitReported(drawn));
726738
require(premiumAmount <= premium, SurplusDeficitReported(premium));
727739
}

tests/unit/Hub/Hub.RefreshPremium.t.sol

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ contract HubRefreshPremiumTest is HubBase {
2222
}
2323

2424
function test_refreshPremium_emitsEvent() public {
25-
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(daiAssetId);
25+
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(hub1, daiAssetId);
2626
(, uint256 premiumBefore) = hub1.getAssetOwed(daiAssetId);
2727

2828
DataTypes.PremiumDelta memory premiumDelta = DataTypes.PremiumDelta({
@@ -39,7 +39,7 @@ contract HubRefreshPremiumTest is HubBase {
3939
(, uint256 premiumAfter) = hub1.getAssetOwed(daiAssetId);
4040

4141
assertEq(
42-
_loadAssetPremiumData(daiAssetId),
42+
_loadAssetPremiumData(hub1, daiAssetId),
4343
_applyPremiumDelta(premiumDataBefore, premiumDelta)
4444
);
4545
assertLe(premiumAfter - premiumBefore, 2, 'premium should not increase by more than 2');
@@ -62,7 +62,7 @@ contract HubRefreshPremiumTest is HubBase {
6262
});
6363

6464
uint256 assetId = daiAssetId;
65-
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(assetId);
65+
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(hub1, assetId);
6666
(, uint256 premiumBefore) = hub1.getAssetOwed(daiAssetId);
6767
bool reverting;
6868

@@ -79,7 +79,10 @@ contract HubRefreshPremiumTest is HubBase {
7979
(, uint256 premiumAfter) = hub1.getAssetOwed(daiAssetId);
8080

8181
if (!reverting) {
82-
assertEq(_loadAssetPremiumData(assetId), _applyPremiumDelta(premiumDataBefore, premiumDelta));
82+
assertEq(
83+
_loadAssetPremiumData(hub1, assetId),
84+
_applyPremiumDelta(premiumDataBefore, premiumDelta)
85+
);
8386
assertLe(premiumAfter - premiumBefore, 2, 'premium should not increase by more than 2');
8487
}
8588
}
@@ -90,7 +93,7 @@ contract HubRefreshPremiumTest is HubBase {
9093
Utils.borrow(spoke1, _daiReserveId(spoke1), bob, 5000e18, bob);
9194

9295
DataTypes.Asset memory asset = hub1.getAsset(assetId);
93-
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(assetId);
96+
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(hub1, assetId);
9497
(, uint256 premiumBefore) = hub1.getAssetOwed(daiAssetId);
9598

9699
sharesDeltaPos = bound(sharesDeltaPos, 0, asset.premiumShares.toInt256());
@@ -110,7 +113,10 @@ contract HubRefreshPremiumTest is HubBase {
110113

111114
(, uint256 premiumAfter) = hub1.getAssetOwed(daiAssetId);
112115

113-
assertEq(_loadAssetPremiumData(assetId), _applyPremiumDelta(premiumDataBefore, premiumDelta));
116+
assertEq(
117+
_loadAssetPremiumData(hub1, assetId),
118+
_applyPremiumDelta(premiumDataBefore, premiumDelta)
119+
);
114120
assertLe(premiumAfter - premiumBefore, 2, 'premium should not increase by more than 2');
115121
}
116122

@@ -126,7 +132,7 @@ contract HubRefreshPremiumTest is HubBase {
126132
Utils.borrow(spoke1, _daiReserveId(spoke1), bob, 1e18, bob);
127133

128134
DataTypes.Asset memory asset = hub1.getAsset(assetId);
129-
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(assetId);
135+
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(hub1, assetId);
130136
(, uint256 premiumBefore) = hub1.getAssetOwed(daiAssetId);
131137
bool reverting;
132138

@@ -169,7 +175,10 @@ contract HubRefreshPremiumTest is HubBase {
169175
(, uint256 premiumAfter) = hub1.getAssetOwed(daiAssetId);
170176

171177
if (!reverting) {
172-
assertEq(_loadAssetPremiumData(assetId), _applyPremiumDelta(premiumDataBefore, premiumDelta));
178+
assertEq(
179+
_loadAssetPremiumData(hub1, assetId),
180+
_applyPremiumDelta(premiumDataBefore, premiumDelta)
181+
);
173182
assertLe(premiumAfter - premiumBefore, 2, 'premium should not increase by more than 2');
174183
}
175184
}
@@ -191,7 +200,7 @@ contract HubRefreshPremiumTest is HubBase {
191200
Utils.borrow(spoke1, _daiReserveId(spoke1), bob, 1e18, bob);
192201

193202
DataTypes.Asset memory asset = hub1.getAsset(assetId);
194-
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(assetId);
203+
PremiumDataLocal memory premiumDataBefore = _loadAssetPremiumData(hub1, assetId);
195204
(, uint256 premiumBefore) = hub1.getAssetOwed(daiAssetId);
196205
bool reverting;
197206

@@ -238,13 +247,55 @@ contract HubRefreshPremiumTest is HubBase {
238247
(, uint256 premiumAfter) = hub1.getAssetOwed(daiAssetId);
239248

240249
if (!reverting) {
241-
assertEq(_loadAssetPremiumData(assetId), _applyPremiumDelta(premiumDataBefore, premiumDelta));
250+
assertEq(
251+
_loadAssetPremiumData(hub1, assetId),
252+
_applyPremiumDelta(premiumDataBefore, premiumDelta)
253+
);
242254
assertLe(premiumAfter - premiumBefore, 2, 'premium should not increase by more than 2');
243255
}
244256
}
245257

246-
function _loadAssetPremiumData(uint256 assetId) internal view returns (PremiumDataLocal memory) {
247-
DataTypes.Asset memory asset = hub1.getAsset(assetId);
258+
function test_refreshPremium_spokePremiumUpdateIsContained() public {
259+
uint256 assetId = daiAssetId;
260+
Utils.supplyCollateral(spoke1, _daiReserveId(spoke1), bob, MAX_SUPPLY_AMOUNT, bob);
261+
Utils.borrow(spoke1, _daiReserveId(spoke1), bob, 5000e18, bob);
262+
Utils.supplyCollateral(spoke2, _daiReserveId(spoke2), alice, 10000e18, alice);
263+
Utils.borrow(spoke2, _daiReserveId(spoke2), alice, 5000e18, alice);
264+
265+
skip(322 days);
266+
267+
uint256 spoke1AccruedPremium = _getSpokeAccruedPremium(hub1, assetId, address(spoke1));
268+
uint256 spoke2AccruedPremium = _getSpokeAccruedPremium(hub1, assetId, address(spoke2));
269+
assertGt(spoke1AccruedPremium, 0);
270+
assertGt(spoke2AccruedPremium, 0);
271+
272+
vm.expectRevert(stdError.arithmeticError);
273+
// realize premium by manipulating offset
274+
vm.prank(address(spoke1));
275+
hub1.refreshPremium(
276+
assetId,
277+
DataTypes.PremiumDelta({
278+
sharesDelta: 0,
279+
offsetDelta: (spoke1AccruedPremium + spoke2AccruedPremium).toInt256(),
280+
realizedDelta: (spoke1AccruedPremium + spoke2AccruedPremium).toInt256()
281+
})
282+
);
283+
}
284+
285+
function _getSpokeAccruedPremium(
286+
IHub hub,
287+
uint256 assetId,
288+
address spoke
289+
) internal view returns (uint256) {
290+
DataTypes.SpokeData memory spokeData = hub.getSpoke(assetId, spoke);
291+
return hub.previewRestoreByShares(assetId, spokeData.premiumShares) - spokeData.premiumOffset;
292+
}
293+
294+
function _loadAssetPremiumData(
295+
IHub hub,
296+
uint256 assetId
297+
) internal view returns (PremiumDataLocal memory) {
298+
DataTypes.Asset memory asset = hub.getAsset(assetId);
248299
return PremiumDataLocal(asset.premiumShares, asset.premiumOffset, asset.realizedPremium);
249300
}
250301

0 commit comments

Comments
 (0)