Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions snapshots/NativeTokenGateway.Operations.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"borrowNative": "227997",
"borrowNative": "230143",
"repayNative": "166265",
"supplyAsCollateralNative": "160205",
"supplyAsCollateralNative": "163411",
"supplyNative": "135802",
"withdrawNative: full": "125580",
"withdrawNative: partial": "136774"
Expand Down
6 changes: 3 additions & 3 deletions snapshots/SignatureGateway.Operations.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"borrowWithSig": "213175",
"borrowWithSig": "215321",
"repayWithSig": "186537",
"setSelfAsUserPositionManagerWithSig": "75385",
"setUsingAsCollateralWithSig": "85409",
"setUsingAsCollateralWithSig": "88615",
"supplyWithSig": "152034",
"updateUserDynamicConfigWithSig": "63129",
"updateUserDynamicConfigWithSig": "65274",
"updateUserRiskPremiumWithSig": "62099",
"withdrawWithSig": "130834"
}
30 changes: 15 additions & 15 deletions snapshots/Spoke.Operations.ZeroRiskPremium.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
{
"borrow: first": "189605",
"borrow: second action, same reserve": "169471",
"borrow: first": "191744",
"borrow: second action, same reserve": "171610",
"liquidationCall (receiveShares): full": "295586",
"liquidationCall (receiveShares): partial": "295304",
"liquidationCall (reportDeficit): full": "350919",
"liquidationCall: full": "305078",
"liquidationCall: partial": "304796",
"permitReserve + repay (multicall)": "164506",
"permitReserve + supply (multicall)": "146828",
"permitReserve + supply + enable collateral (multicall)": "161301",
"permitReserve + supply + enable collateral (multicall)": "164507",
"repay: full": "123800",
"repay: partial": "128770",
"setUserPositionManagersWithSig: disable": "47039",
"setUserPositionManagersWithSig: enable": "68951",
"supply + enable collateral (multicall)": "141481",
"supply + enable collateral (multicall)": "144687",
"supply: 0 borrows, collateral disabled": "122874",
"supply: 0 borrows, collateral enabled": "105845",
"supply: second action, same reserve": "105774",
"updateUserDynamicConfig: 1 collateral": "74603",
"updateUserDynamicConfig: 2 collaterals": "89520",
"updateUserDynamicConfig: 1 collateral": "76744",
"updateUserDynamicConfig: 2 collaterals": "91661",
"updateUserRiskPremium: 1 borrow": "94940",
"updateUserRiskPremium: 2 borrows": "104075",
"usingAsCollateral: 0 borrows, enable": "59638",
"usingAsCollateral: 1 borrow, disable": "104981",
"usingAsCollateral: 1 borrow, enable": "42526",
"usingAsCollateral: 2 borrows, disable": "126089",
"usingAsCollateral: 2 borrows, enable": "42538",
"withdraw: 0 borrows, full": "127997",
"withdraw: 0 borrows, partial": "132732",
"withdraw: 1 borrow, partial": "159169",
"withdraw: 2 borrows, partial": "173287",
"usingAsCollateral: 0 borrows, enable": "62844",
"usingAsCollateral: 1 borrow, disable": "107123",
"usingAsCollateral: 1 borrow, enable": "45732",
"usingAsCollateral: 2 borrows, disable": "128231",
"usingAsCollateral: 2 borrows, enable": "45744",
"withdraw: 0 borrows, full": "130135",
"withdraw: 0 borrows, partial": "134871",
"withdraw: 1 borrow, partial": "161308",
"withdraw: 2 borrows, partial": "175425",
"withdraw: non collateral": "105920"
}
44 changes: 22 additions & 22 deletions snapshots/Spoke.Operations.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
{
"borrow: first": "258588",
"borrow: second action, same reserve": "201454",
"liquidationCall (receiveShares): full": "327674",
"liquidationCall (receiveShares): partial": "327392",
"liquidationCall (reportDeficit): full": "345548",
"liquidationCall: full": "337166",
"liquidationCall: partial": "336884",
"borrow: first": "260734",
"borrow: second action, same reserve": "203600",
"liquidationCall (receiveShares): full": "327687",
"liquidationCall (receiveShares): partial": "327405",
"liquidationCall (reportDeficit): full": "345561",
"liquidationCall: full": "337179",
"liquidationCall: partial": "336897",
"permitReserve + repay (multicall)": "161988",
"permitReserve + supply (multicall)": "146828",
"permitReserve + supply + enable collateral (multicall)": "161301",
"permitReserve + supply + enable collateral (multicall)": "164507",
"repay: full": "117879",
"repay: partial": "137249",
"setUserPositionManagersWithSig: disable": "47039",
"setUserPositionManagersWithSig: enable": "68951",
"supply + enable collateral (multicall)": "141481",
"supply + enable collateral (multicall)": "144687",
"supply: 0 borrows, collateral disabled": "122874",
"supply: 0 borrows, collateral enabled": "105845",
"supply: second action, same reserve": "105774",
"updateUserDynamicConfig: 1 collateral": "74603",
"updateUserDynamicConfig: 2 collaterals": "89520",
"updateUserRiskPremium: 1 borrow": "148344",
"updateUserRiskPremium: 2 borrows": "198096",
"usingAsCollateral: 0 borrows, enable": "59638",
"usingAsCollateral: 1 borrow, disable": "158385",
"usingAsCollateral: 1 borrow, enable": "42526",
"usingAsCollateral: 2 borrows, disable": "228110",
"usingAsCollateral: 2 borrows, enable": "42538",
"withdraw: 0 borrows, full": "127997",
"withdraw: 0 borrows, partial": "132732",
"withdraw: 1 borrow, partial": "210071",
"withdraw: 2 borrows, partial": "255841",
"updateUserDynamicConfig: 1 collateral": "76744",
"updateUserDynamicConfig: 2 collaterals": "91661",
Copy link
Member

@miguelmtzinf miguelmtzinf Feb 5, 2026

Choose a reason for hiding this comment

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

shouldn't be the same increase as for 2 collaterals? ~2.1k
gas increase isn't flat now?

Copy link
Contributor

Choose a reason for hiding this comment

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

the gas increase between both old snasphots and new ones is flat here (+2141 on both)

"updateUserRiskPremium: 1 borrow": "148357",
"updateUserRiskPremium: 2 borrows": "198109",
"usingAsCollateral: 0 borrows, enable": "62844",
"usingAsCollateral: 1 borrow, disable": "160531",
"usingAsCollateral: 1 borrow, enable": "45732",
"usingAsCollateral: 2 borrows, disable": "230255",
"usingAsCollateral: 2 borrows, enable": "45744",
"withdraw: 0 borrows, full": "130135",
"withdraw: 0 borrows, partial": "134871",
"withdraw: 1 borrow, partial": "212216",
"withdraw: 2 borrows, partial": "257987",
"withdraw: non collateral": "105920"
}
7 changes: 5 additions & 2 deletions src/spoke/Spoke.sol
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ abstract contract Spoke is
address user
) internal returns (UserAccountData memory) {
UserAccountData memory accountData = _processUserAccountData(user, true);
emit RefreshAllUserDynamicConfig(user);
emit RefreshUserDynamicConfig(user, _positionStatus[user].getCollateralBitmap(_reserveCount));
require(
accountData.healthFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
HealthFactorBelowThreshold()
Expand Down Expand Up @@ -813,7 +813,10 @@ abstract contract Spoke is

function _refreshDynamicConfig(address user, uint256 reserveId) internal {
_userPositions[user][reserveId].dynamicConfigKey = _reserves[reserveId].dynamicConfigKey;
emit RefreshSingleUserDynamicConfig(user, reserveId);
emit RefreshUserDynamicConfig(
user,
PositionStatusMap.getSingleCollateralBitmap(reserveId, _reserveCount)
);
}

/// @notice Refreshes premium for borrowed reserves of `user` with `newRiskPremium`.
Expand Down
8 changes: 2 additions & 6 deletions src/spoke/interfaces/ISpoke.sol
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,8 @@ interface ISpoke is ISpokeBase, IAccessManaged, IIntentConsumer, IExtSload, IMul

/// @notice Emitted when a user's dynamic config is refreshed for all reserves to their latest config key.
/// @param user The address of the user.
event RefreshAllUserDynamicConfig(address indexed user);

/// @notice Emitted when a user's dynamic config is refreshed for a single reserve to its latest config key.
/// @param user The address of the user.
/// @param reserveId The identifier of the reserve.
event RefreshSingleUserDynamicConfig(address indexed user, uint256 reserveId);
/// @param collateralBitmap The collateral bitmap containing only the collateral bits for the user.
event RefreshUserDynamicConfig(address indexed user, bytes collateralBitmap);

/// @notice Emitted on updateUserRiskPremium action.
/// @param user The owner of the position being modified.
Expand Down
91 changes: 91 additions & 0 deletions src/spoke/libraries/PositionStatusMap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,95 @@ library PositionStatusMap {
ret := and(word, shr(sub(256, shl(1, mod(reserveCount, 128))), COLLATERAL_MASK))
}
}

/// @notice Returns the collateral bitmap as bytes.
/// @dev Each bucket (128 reserves) is compressed to 16 bytes (uint128), with two buckets packed per 32-byte word.
/// @param reserveCount The current reserveCount to determine bucket boundaries.
/// @return The compressed collateral bitmap (16 bytes per bucket).
function getCollateralBitmap(
ISpoke.PositionStatus storage self,
uint256 reserveCount
) internal view returns (bytes memory) {
unchecked {
uint256 bucketCount = reserveCount == 0 ? 0 : (reserveCount - 1).bucketId() + 1;
bytes memory result = new bytes(((bucketCount + 1) / 2) * 32);

for (uint256 i; i < bucketCount; ) {
uint128 compressed0 = self.map[i].compressCollateral();
// Compress next bucket if it exists, otherwise 0
uint128 compressed1 = (i + 1 < bucketCount) ? self.map[i + 1].compressCollateral() : 0;

assembly ('memory-safe') {
// Pack with compressed0 in lower 128 bits, compressed1 in upper 128 bits
// This ensures reserve N is at bit N in the continuous bitmap
let packed := or(compressed0, shl(128, compressed1))
mstore(add(add(result, 32), shl(5, shr(1, i))), packed)
}

i += 2;
}

return result;
}
}

/// @notice Returns the collateral bitmap with a signle reserve as bytes.
/// @param reserveId The reserveId to set in the bitmap.
/// @param reserveCount The current reserveCount to determine bucket boundaries.
/// @return The compressed collateral bitmap (16 bytes per bucket).
function getSingleCollateralBitmap(
uint256 reserveId,
uint256 reserveCount
) internal pure returns (bytes memory) {
unchecked {
bytes memory result = new bytes(((reserveCount / 256) + 1) * 32);

uint256 bucket = reserveId / 256;
uint256 positionInBucket = reserveId % 256;

assembly ('memory-safe') {
let wordPtr := add(add(result, 32), shl(5, bucket))
let currentWord := mload(wordPtr)
let updatedWord := or(currentWord, shl(positionInBucket, 1))
mstore(wordPtr, updatedWord)
}

return result;
}
}

/// @notice Compresses collateral bits from a word into a densely packed uint128.
/// @dev Extracts odd bits (1,3,5,...,255) representing collateral flags for 128 reserves,
/// and packs them into consecutive bits (0,1,2,...,127) in a uint128.
/// @param word The word to compress.
/// @return packed The compressed uint128 containing only collateral bits.
function compressCollateral(uint256 word) internal pure returns (uint128 packed) {
assembly ('memory-safe') {
// (word & COLLATERAL_MASK) >> 1 moves bits from positions 1,3,5,... to 0,2,4,...
let x := shr(1, and(word, COLLATERAL_MASK))

// Progressively merge bits from sparse even positions into dense consecutive positions
// Merge bit pairs from positions (0,2) to (0,1), (4,6) to (4,5), ...
x := and(or(x, shr(1, x)), 0x3333333333333333333333333333333333333333333333333333333333333333)
// Merge nibbles from positions (0,1,4,5) to (0-3), (8,9,12,13) to (8-11), ...
x := and(or(x, shr(2, x)), 0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F)
// Merge bytes from positions (0-3, 8-11) to (0-7), (16-19, 24-27) to (16-23), ...
x := and(or(x, shr(4, x)), 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF)
// Merge 16-bit groups from positions (0-7, 16-23) to (0-15), (32-39, 48-55) to (32-47), ...
x := and(or(x, shr(8, x)), 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF)
// Merge 32-bit groups from positions (0-15, 32-47) to (0-31), (64-79, 96-111) to (64-95), ...
x := and(
or(x, shr(16, x)),
0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF
)
// Merge 64-bit groups from positions (0-31, 64-95) to (0-63), (128-159, 192-223) to (128-191).
x := and(
or(x, shr(32, x)),
0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF
)

// Move all 128 bits now in lower half
packed := and(or(x, shr(64, x)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
}
}
}
5 changes: 1 addition & 4 deletions tests/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2720,10 +2720,7 @@ abstract contract Base is Test {
}

function _assertDynamicConfigRefreshEventsNotEmitted() internal {
_assertEventsNotEmitted(
ISpoke.RefreshAllUserDynamicConfig.selector,
ISpoke.RefreshSingleUserDynamicConfig.selector
);
_assertEventNotEmitted(ISpoke.RefreshUserDynamicConfig.selector);
}

// @dev Helper function to get asset position, valid if no time has passed since last action
Expand Down
15 changes: 15 additions & 0 deletions tests/mocks/PositionStatusMapWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ contract PositionStatusMapWrapper {
return _p.nextCollateral(startReserveId);
}

function getCollateralBitmap(uint256 reserveCount) external view returns (bytes memory) {
return _p.getCollateralBitmap(reserveCount);
}

function getSingleCollateralBitmap(
uint256 reserveId,
uint256 reserveCount
) external pure returns (bytes memory) {
return PositionStatusMap.getSingleCollateralBitmap(reserveId, reserveCount);
}

function compressCollateral(uint256 bucket) external pure returns (uint128) {
return PositionStatusMap.compressCollateral(bucket);
}

function slot() external pure returns (bytes32 s) {
assembly ('memory-safe') {
s := _p.slot
Expand Down
30 changes: 24 additions & 6 deletions tests/unit/Spoke/Spoke.DynamicConfig.Triggers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,11 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {
configs = _getUserDynConfigKeys(spoke1, alice);
Utils.supplyCollateral(spoke1, _wethReserveId(spoke1), alice, 1e18, alice);

uint256[] memory collateralReserves = new uint256[](2);
collateralReserves[0] = _usdxReserveId(spoke1);
collateralReserves[1] = _wethReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(collateralReserves));
Utils.borrow(spoke1, _daiReserveId(spoke1), alice, 100e18, alice);

assertNotEq(_getUserDynConfigKeys(spoke1, alice), configs);
Expand Down Expand Up @@ -166,8 +169,11 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {
configs = _getUserDynConfigKeys(spoke1, alice);
Utils.supplyCollateral(spoke1, _wethReserveId(spoke1), alice, 1e18, alice);

uint256[] memory collateralReserves = new uint256[](2);
collateralReserves[0] = _usdxReserveId(spoke1);
collateralReserves[1] = _wethReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(collateralReserves));
Utils.withdraw(spoke1, _usdxReserveId(spoke1), alice, 500e6, alice);

assertNotEq(_getUserDynConfigKeys(spoke1, alice), configs);
Expand Down Expand Up @@ -195,7 +201,10 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {

// when enabling, only the relevant asset is refreshed
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshSingleUserDynamicConfig(alice, _wethReserveId(spoke1));
emit ISpoke.RefreshUserDynamicConfig(
alice,
_buildCollateralBitmap(_wethReserveId(spoke1), spoke1.getReserveCount())
);
vm.prank(alice);
spoke1.setUsingAsCollateral(_wethReserveId(spoke1), true, alice);

Expand All @@ -206,8 +215,11 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {
assertNotEq(abi.encode(userConfig), abi.encode(spokeConfig));

// when disabling all configs are refreshed
// Note: bitmap reflects state AFTER the collateral is disabled, so only WETH remains
uint256[] memory remainingCollateral = new uint256[](1);
remainingCollateral[0] = _wethReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(remainingCollateral));
vm.prank(alice);
spoke1.setUsingAsCollateral(_usdxReserveId(spoke1), false, alice);

Expand All @@ -228,8 +240,11 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {
assertNotEq(_getSpokeDynConfigKeys(spoke1), configs);

// manually trigger update
uint256[] memory collateralReserves = new uint256[](2);
collateralReserves[0] = _usdxReserveId(spoke1);
collateralReserves[1] = _wethReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(collateralReserves));
vm.prank(alice);
spoke1.updateUserDynamicConfig(alice);

Expand Down Expand Up @@ -337,8 +352,11 @@ contract SpokeDynamicConfigTriggersTest is SpokeBase {
) internal {
uint256 snapshotId = vm.snapshotState();

uint256[] memory collateralReserves = new uint256[](2);
collateralReserves[0] = _usdxReserveId(spoke1);
collateralReserves[1] = _wethReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(collateralReserves));
vm.prank(caller);
spoke1.updateUserDynamicConfig(alice);

Expand Down
5 changes: 4 additions & 1 deletion tests/unit/Spoke/Spoke.PositionManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,11 @@ contract SpokePositionManagerTest is SpokeBase {

_approvePositionManager(alice);

uint256[] memory collateralReserves = new uint256[](2);
collateralReserves[0] = _wethReserveId(spoke1);
collateralReserves[1] = _daiReserveId(spoke1);
vm.expectEmit(address(spoke1));
emit ISpoke.RefreshAllUserDynamicConfig(alice);
emit ISpoke.RefreshUserDynamicConfig(alice, _buildCollateralBitmap(collateralReserves));
vm.prank(POSITION_MANAGER);
spoke1.updateUserDynamicConfig(alice);

Expand Down
Loading
Loading