Skip to content

Commit d39185f

Browse files
authored
feat: make update user rp reserveId agnostic (#404)
* feat: make update user rp reserveId agnostic * fix: rm hf check on updateUserRiskPremium * feat: use custom multicall * fix: natspec * fix: correct revert cond * test: begin user rp tests * chore: comment * feat: emit reserve config on addReserve, fix test name
1 parent 24cfad0 commit d39185f

File tree

11 files changed

+162
-117
lines changed

11 files changed

+162
-117
lines changed

snapshots/Spoke.Getters.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"getUserAccountData: supplies: 0, borrows: 0": "23895",
3-
"getUserAccountData: supplies: 1, borrows: 0": "73141",
4-
"getUserAccountData: supplies: 2, borrows: 0": "117432",
5-
"getUserAccountData: supplies: 2, borrows: 1": "146405",
6-
"getUserAccountData: supplies: 2, borrows: 2": "173848"
2+
"getUserAccountData: supplies: 0, borrows: 0": "23917",
3+
"getUserAccountData: supplies: 1, borrows: 0": "73163",
4+
"getUserAccountData: supplies: 2, borrows: 0": "117454",
5+
"getUserAccountData: supplies: 2, borrows: 1": "146427",
6+
"getUserAccountData: supplies: 2, borrows: 2": "173870"
77
}

snapshots/Spoke.Operations.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"borrow": "481621",
3-
"repay: full": "221810",
4-
"repay: partial": "290017",
2+
"borrow": "481646",
3+
"repay: full": "221830",
4+
"repay: partial": "290042",
55
"supply: 0 debt, collateralDisabled": "171802",
66
"supply: 0 debt, collateralEnabled": "171778",
77
"supply: 1 debt": "171826",
88
"supply: 2 debt": "171837",
99
"supply: 3 debt": "171790",
10-
"updateUserRiskPremium": "186410",
11-
"updateUserRiskPremium: 2 debts": "227359",
10+
"updateUserRiskPremium": "172555",
11+
"updateUserRiskPremium: 2 debts": "213546",
1212
"usingAsCollateral": "50661",
13-
"withdraw: full": "218158",
14-
"withdraw: partial": "229109"
13+
"withdraw: full": "218183",
14+
"withdraw: partial": "229134"
1515
}

src/contracts/Spoke.sol

Lines changed: 23 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.0;
33

4-
import {Multicall} from 'src/dependencies/openzeppelin/Multicall.sol';
4+
import {Multicall} from 'src/misc/Multicall.sol';
55

66
// libraries
77
import {WadRayMath} from 'src/libraries/math/WadRayMath.sol';
@@ -88,6 +88,7 @@ contract Spoke is ISpoke, Multicall {
8888
});
8989

9090
emit ReserveAdded(reserveId, assetId);
91+
emit ReserveConfigUpdated(reserveId, config);
9192

9293
return reserveId;
9394
}
@@ -311,6 +312,7 @@ contract Spoke is ISpoke, Multicall {
311312
emit Repay(reserveId, msg.sender, restoredShares);
312313
}
313314

315+
/// @inheritdoc ISpoke
314316
function setUsingAsCollateral(uint256 reserveId, bool usingAsCollateral) external {
315317
DataTypes.Reserve storage reserve = _reserves[reserveId];
316318
DataTypes.UserPosition storage userPosition = _userPositions[msg.sender][reserveId];
@@ -323,58 +325,13 @@ contract Spoke is ISpoke, Multicall {
323325
emit UsingAsCollateral(reserveId, msg.sender, usingAsCollateral);
324326
}
325327

326-
/// @dev Must be called on a reserve user is already borrowing
327-
/// @dev If not called by position owner or DAO, reverts if user risk premium increases
328-
function updateUserRiskPremium(uint256 reserveId, address user) external {
329-
DataTypes.Reserve storage reserve = _reserves[reserveId];
330-
DataTypes.UserPosition storage userPosition = _userPositions[user][reserveId];
331-
require(_isBorrowing(userPosition), UserNotBorrowingReserve(reserveId));
332-
uint256 assetId = reserve.assetId;
333-
334-
uint256 userPremiumDrawnShares = userPosition.premiumDrawnShares;
335-
uint256 userPremiumOffset = userPosition.premiumOffset;
336-
uint256 accruedPremium = HUB.convertToDrawnAssets(assetId, userPremiumDrawnShares) -
337-
userPremiumOffset; // assets(premiumShares) - offset should never be < 0
338-
userPosition.premiumDrawnShares = 0;
339-
userPosition.premiumOffset = 0;
340-
userPosition.realizedPremium += accruedPremium;
341-
342-
_refreshPremiumDebt(
343-
reserve,
344-
user,
345-
assetId,
346-
-int256(userPremiumDrawnShares),
347-
-int256(userPremiumOffset),
348-
int256(accruedPremium)
349-
);
350-
351-
uint256 newUserRiskPremium = _validateUserPosition(user); // validates HF
352-
353-
uint256 newUserPremiumDrawnShares = userPosition.premiumDrawnShares = userPosition
354-
.baseDrawnShares
355-
.percentMul(newUserRiskPremium);
356-
// TODO: With access control, also allow DAO to update user risk premium in case of increase
357-
// Check new premium drawn shares as proxy for user risk premium
358-
require(
359-
msg.sender == user || newUserPremiumDrawnShares < userPremiumDrawnShares,
360-
NoUserRiskPremiumDecrease()
361-
);
362-
userPremiumOffset = userPosition.premiumOffset = HUB.previewOffset(
363-
assetId,
364-
userPosition.premiumDrawnShares
365-
);
366-
367-
_refreshPremiumDebt(
368-
reserve,
369-
user,
370-
assetId,
371-
int256(newUserPremiumDrawnShares),
372-
int256(userPremiumOffset),
373-
0
374-
);
375-
_notifyRiskPremiumUpdate(assetId, user, newUserRiskPremium);
376-
377-
emit UserRiskPremiumUpdate(user, newUserRiskPremium);
328+
/// @inheritdoc ISpoke
329+
function updateUserRiskPremium(address user) external {
330+
(uint256 userRiskPremium, , , , ) = _calculateUserAccountData(user);
331+
bool premiumIncrease = _notifyRiskPremiumUpdate(type(uint256).max, user, userRiskPremium);
332+
// todo allow authorized caller to increase as well
333+
require(msg.sender == user || !premiumIncrease, Unauthorized());
334+
emit UserRiskPremiumUpdate(user, userRiskPremium);
378335
}
379336

380337
function getUsingAsCollateral(uint256 reserveId, address user) external view returns (bool) {
@@ -441,6 +398,7 @@ contract Spoke is ISpoke, Multicall {
441398
(, , uint256 healthFactor, , ) = _calculateUserAccountData(user);
442399
return healthFactor;
443400
}
401+
444402
function getReservePrice(uint256 reserveId) public view returns (uint256) {
445403
return oracle.getAssetPrice(_reserves[reserveId].assetId);
446404
}
@@ -841,7 +799,8 @@ contract Spoke is ISpoke, Multicall {
841799
uint256 assetIdToAvoid,
842800
address userAddress,
843801
uint256 newUserRiskPremium
844-
) internal {
802+
) internal returns (bool) {
803+
bool premiumIncrease;
845804
uint256 reserveCount_ = reserveCount;
846805
uint256 reserveId;
847806
while (reserveId < reserveCount_) {
@@ -861,11 +820,17 @@ contract Spoke is ISpoke, Multicall {
861820
userPosition.premiumOffset = HUB.previewOffset(assetId, userPosition.premiumDrawnShares);
862821
userPosition.realizedPremium += accruedUserPremium;
863822

823+
int256 premiumDrawnSharesDelta = _signedDiff(
824+
userPosition.premiumDrawnShares,
825+
oldUserPremiumDrawnShares
826+
);
827+
if (!premiumIncrease) premiumIncrease = premiumDrawnSharesDelta > 0;
828+
864829
_refreshPremiumDebt(
865830
reserve,
866831
userAddress,
867832
assetId,
868-
_signedDiff(userPosition.premiumDrawnShares, oldUserPremiumDrawnShares),
833+
premiumDrawnSharesDelta,
869834
_signedDiff(userPosition.premiumOffset, oldUserPremiumOffset),
870835
int256(accruedUserPremium)
871836
);
@@ -874,6 +839,7 @@ contract Spoke is ISpoke, Multicall {
874839
++reserveId;
875840
}
876841
}
842+
return premiumIncrease;
877843
}
878844

879845
function _validateUserPosition(address userAddress) internal view returns (uint256) {
@@ -903,7 +869,7 @@ contract Spoke is ISpoke, Multicall {
903869
return int256(a) - int256(b); // todo use safeCast when amounts packed to uint112/uint128
904870
}
905871

906-
function _validateLiquidationConfig(DataTypes.LiquidationConfig calldata config) internal view {
872+
function _validateLiquidationConfig(DataTypes.LiquidationConfig calldata config) internal pure {
907873
_validateCloseFactor(config.closeFactor);
908874
// if liquidationBonusFactor == 0, then variable liquidation bonus will not be applied
909875
require(
@@ -917,7 +883,7 @@ contract Spoke is ISpoke, Multicall {
917883
);
918884
}
919885

920-
function _validateCloseFactor(uint256 closeFactor) internal view {
886+
function _validateCloseFactor(uint256 closeFactor) internal pure {
921887
require(closeFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD, InvalidCloseFactor());
922888
}
923889
}

src/dependencies/openzeppelin/Multicall.sol

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/interfaces/IMulticall.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
interface IMulticall {
5+
/**
6+
* @notice Call multiple functions in the current contract and return the data from all of them if they all succeed.
7+
* @param data The encoded function data for each of the calls to make to this contract.
8+
* @return results The results from each of the calls passed in via data.
9+
*/
10+
function multicall(bytes[] calldata data) external returns (bytes[] memory results);
11+
}

src/interfaces/ISpoke.sol

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.0;
33

4+
import {IMulticall} from 'src/interfaces/IMulticall.sol';
45
import {DataTypes} from 'src/libraries/types/DataTypes.sol';
56

67
/**
78
* @title ISpoke
89
* @author Aave Labs
910
* @notice Basic interface for Spoke
1011
*/
11-
interface ISpoke {
12+
interface ISpoke is IMulticall {
1213
event ReserveAdded(uint256 indexed reserveId, uint256 indexed assetId);
1314
event ReserveConfigUpdated(uint256 indexed reserveId, DataTypes.ReserveConfig config);
1415
event LiquidityPremiumUpdated(uint256 indexed reserveId, uint256 liquidityPremium);
@@ -59,13 +60,15 @@ interface ISpoke {
5960
error InvalidHubAddress();
6061
error InvalidHealthFactorBonusThreshold();
6162
error InvalidLiquidationBonusFactor();
62-
error NoUserRiskPremiumDecrease();
63+
error Unauthorized();
6364

6465
function addReserve(
6566
uint256 assetId,
6667
DataTypes.ReserveConfig memory params
6768
) external returns (uint256);
69+
6870
function updateReserveConfig(uint256 reserveId, DataTypes.ReserveConfig calldata params) external;
71+
6972
function updateLiquidationConfig(DataTypes.LiquidationConfig calldata config) external;
7073

7174
/**
@@ -100,19 +103,41 @@ interface ISpoke {
100103
*/
101104
function repay(uint256 reserveId, uint256 amount) external;
102105

106+
/**
107+
* @notice Allows suppliers to enable/disable a specific supplied reserve as collateral.
108+
* @param reserveId The reserveId of the underlying asset as registered on the spoke.
109+
* @param usingAsCollateral True if the user wants to use the supply as collateral, false otherwise.
110+
*/
103111
function setUsingAsCollateral(uint256 reserveId, bool usingAsCollateral) external;
104-
function updateUserRiskPremium(uint256 reserveId, address user) external;
112+
113+
/**
114+
* @notice Allows updating the risk premium on user position.
115+
* @dev If the risk premium has increased, the caller must be authorized or the owner of the position,
116+
* reverts with `Unauthorized` otherwise.
117+
* @param user The address of the user.
118+
*/
119+
function updateUserRiskPremium(address user) external;
105120

106121
function getCollateralFactor(uint256 reserveId) external view returns (uint256);
122+
107123
function getHealthFactor(address user) external view returns (uint256);
124+
108125
function getLiquidityPremium(uint256 reserveId) external view returns (uint256);
126+
109127
function getReserve(uint256 reserveId) external view returns (DataTypes.Reserve memory);
128+
110129
function getReserveDebt(uint256 reserveId) external view returns (uint256, uint256);
130+
111131
function getReservePrice(uint256 reserveId) external view returns (uint256);
132+
112133
function getReserveRiskPremium(uint256 reserveId) external view returns (uint256);
134+
113135
function getReserveSuppliedAmount(uint256 reserveId) external view returns (uint256);
136+
114137
function getReserveSuppliedShares(uint256 reserveId) external view returns (uint256);
138+
115139
function getReserveTotalDebt(uint256 reserveId) external view returns (uint256);
140+
116141
function getUserAccountData(
117142
address user
118143
)
@@ -125,22 +150,34 @@ interface ISpoke {
125150
uint256 totalCollateralInBaseCurrency,
126151
uint256 totalDebtInBaseCurrency
127152
);
153+
128154
function getUserDebt(uint256 reserveId, address user) external view returns (uint256, uint256);
155+
129156
function getUserPosition(
130157
uint256 reserveId,
131158
address user
132159
) external view returns (DataTypes.UserPosition memory);
160+
133161
function getUserRiskPremium(address user) external view returns (uint256);
162+
134163
function getUserSuppliedAmount(uint256 reserveId, address user) external view returns (uint256);
164+
135165
function getUserSuppliedShares(uint256 reserveId, address user) external view returns (uint256);
166+
136167
function getUserTotalDebt(uint256 reserveId, address user) external view returns (uint256);
168+
137169
function getUsingAsCollateral(uint256 reserveId, address user) external view returns (bool);
170+
138171
function reserveCount() external view returns (uint256);
172+
139173
function reservesList(uint256) external view returns (uint256);
174+
140175
function HEALTH_FACTOR_LIQUIDATION_THRESHOLD() external view returns (uint256);
176+
141177
function getVariableLiquidationBonus(
142178
uint256 reserveId,
143179
uint256 healthFactor
144180
) external view returns (uint256);
181+
145182
function getLiquidationConfig() external view returns (DataTypes.LiquidationConfig memory);
146183
}

src/misc/Multicall.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {IMulticall} from 'src/interfaces/IMulticall.sol';
5+
6+
/**
7+
* @title Multicall
8+
* @author Aave Labs, inspired by the OpenZeppelin Multicall contract
9+
* @notice This contract allows for batching multiple calls into a single call.
10+
*/
11+
abstract contract Multicall is IMulticall {
12+
/// @inheritdoc IMulticall
13+
function multicall(bytes[] calldata data) external returns (bytes[] memory) {
14+
bytes[] memory results = new bytes[](data.length);
15+
for (uint256 i; i < data.length; ++i) {
16+
(bool ok, bytes memory res) = address(this).delegatecall(data[i]);
17+
18+
assembly ('memory-safe') {
19+
if iszero(ok) {
20+
revert(add(res, 32), mload(res)) // bubble up first revert
21+
}
22+
}
23+
24+
results[i] = res;
25+
}
26+
}
27+
}

tests/Base.t.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,11 @@ abstract contract Base is Test {
717717
return price.percentMul(percent);
718718
}
719719

720+
function setNewPrice(uint256 assetId, uint256 percent) public {
721+
uint256 newPrice = calcNewPrice(oracle.getAssetPrice(assetId), percent);
722+
oracle.setAssetPrice(assetId, newPrice);
723+
}
724+
720725
/// @dev Helper function to calculate asset amount corresponding to single drawn share
721726
function minimumAssetsPerDrawnShare(uint256 assetId) internal view returns (uint256) {
722727
return hub.convertToDrawnAssets(assetId, 1);

0 commit comments

Comments
 (0)