The difference in gas costs is even greater when the element to be removed is at the end of the array. This is due to the reduced number of swaps and pops required to remove elements from the array.
Let’s compare the gas costs of the current implementation with those of the proposed optimized function. Both implementations will use a structure similar to the one used in GlobalConfiguration.sol
to provide the best approximation of gas costs:
import { EnumerableSet } from "@openzeppelin/utils/structs/EnumerableSet.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { console } from "forge-std/Test.sol";
library CurrentRemoveCollateral {
using EnumerableSet for *;
struct Data {
EnumerableSet.AddressSet collateralLiquidationPriority;
}
bytes32 internal constant GLOBAL_CONFIGURATION_LOCATION = keccak256(
abi.encode(uint256(keccak256("fi.zaros.perpetuals.GlobalConfiguration")) - 1)
) & ~bytes32(uint256(0xff));
function load() internal pure returns (Data storage config) {
bytes32 slot = GLOBAL_CONFIGURATION_LOCATION;
assembly {
config.slot := slot
}
}
function configureCollateralLiquidationPriority(Data storage self, address[] memory collateralTypes) internal {
uint256 cachedCollateralTypesCached = collateralTypes.length;
for (uint256 i; i < cachedCollateralTypesCached; i++) {
if (collateralTypes[i] == address(0)) {
revert Errors.ZeroInput("collateralType");
}
if (!self.collateralLiquidationPriority.add(collateralTypes[i])) {
revert Errors.MarginCollateralAlreadyInPriority(collateralTypes[i]);
}
}
}
function removeCollateralFromLiquidationPriority(Data storage self, address collateralType) internal {
bool isInCollateralLiquidationPriority = self.collateralLiquidationPriority.contains(collateralType);
if (!isInCollateralLiquidationPriority) revert();
address[] memory copyCollateralLiquidationPriority = self.collateralLiquidationPriority.values();
uint256 copyCollateralLiquidationPriorityLength = copyCollateralLiquidationPriority.length;
address[] memory newCollateralLiquidationPriority = new address[](copyCollateralLiquidationPriorityLength - 1);
uint256 indexCollateral;
for (uint256 i; i < copyCollateralLiquidationPriorityLength; i++) {
address collateral = copyCollateralLiquidationPriority[i];
self.collateralLiquidationPriority.remove(collateral);
if (collateral == collateralType) {
continue;
}
newCollateralLiquidationPriority[indexCollateral] = collateral;
indexCollateral++;
}
for (uint256 i; i < copyCollateralLiquidationPriorityLength - 1; i++) {
self.collateralLiquidationPriority.add(newCollateralLiquidationPriority[i]);
}
}
}
contract CurrentImplementation {
using CurrentRemoveCollateral for CurrentRemoveCollateral.Data;
using EnumerableSet for EnumerableSet.AddressSet;
function getCollateralLiquidationPriority() external view returns (address[] memory) {
CurrentRemoveCollateral.Data storage self = CurrentRemoveCollateral.load();
uint256 length = self.collateralLiquidationPriority.length();
address[] memory collateralLiquidationPriority = new address[](length);
for (uint256 i; i < length; i++) {
collateralLiquidationPriority[i] = self.collateralLiquidationPriority.at(i);
}
return collateralLiquidationPriority;
}
function configureCollateralLiquidationPriority(address[] memory collateralTypes) external {
CurrentRemoveCollateral.Data storage data = CurrentRemoveCollateral.load();
data.configureCollateralLiquidationPriority(collateralTypes);
}
function removeCollateralFromLiquidationPriority(address collateralType) external {
CurrentRemoveCollateral.Data storage data = CurrentRemoveCollateral.load();
data.removeCollateralFromLiquidationPriority(collateralType);
}
}
library ProposedRemoveCollateral {
using EnumerableSet for *;
struct Data {
EnumerableSet.AddressSet collateralLiquidationPriority;
}
bytes32 internal constant GLOBAL_CONFIGURATION_LOCATION = keccak256(
abi.encode(uint256(keccak256("fi.zaros.perpetuals.GlobalConfiguration")) - 1)
) & ~bytes32(uint256(0xff));
function load() internal pure returns (Data storage config) {
bytes32 slot = GLOBAL_CONFIGURATION_LOCATION;
assembly {
config.slot := slot
}
}
function configureCollateralLiquidationPriority(Data storage self, address[] memory collateralTypes) internal {
uint256 cachedCollateralTypesCached = collateralTypes.length;
for (uint256 i; i < cachedCollateralTypesCached; i++) {
if (collateralTypes[i] == address(0)) {
revert Errors.ZeroInput("collateralType");
}
if (!self.collateralLiquidationPriority.add(collateralTypes[i])) {
revert();
}
}
}
function removeCollateralFromLiquidationPriority(Data storage self, address collateralType) internal {
if (!self.collateralLiquidationPriority.contains(collateralType)) {
revert Errors.MarginCollateralTypeNotInPriority(collateralType);
}
address[] memory values = self.collateralLiquidationPriority.values();
uint256 length = values.length;
uint256 indexToRemove = type(uint256).max;
for (uint256 i = 0; i < length; i++) {
if (values[i] == collateralType) {
indexToRemove = i;
break;
}
}
if (indexToRemove < length) {
for (uint256 i = indexToRemove; i < length; i++) {
self.collateralLiquidationPriority.remove(values[i]);
}
for (uint256 i = indexToRemove + 1; i < length; i++) {
self.collateralLiquidationPriority.add(values[i]);
}
}
}
}
contract ProposedImplementation {
using ProposedRemoveCollateral for ProposedRemoveCollateral.Data;
using EnumerableSet for EnumerableSet.AddressSet;
function getCollateralLiquidationPriority() external view returns (address[] memory) {
ProposedRemoveCollateral.Data storage self = ProposedRemoveCollateral.load();
uint256 length = self.collateralLiquidationPriority.length();
address[] memory collateralLiquidationPriority = new address[](length);
for (uint256 i; i < length; i++) {
collateralLiquidationPriority[i] = self.collateralLiquidationPriority.at(i);
}
return collateralLiquidationPriority;
}
function configureCollateralLiquidationPriority(address[] memory collateralTypes) external {
ProposedRemoveCollateral.Data storage data = ProposedRemoveCollateral.load();
data.configureCollateralLiquidationPriority(collateralTypes);
}
function removeCollateralFromLiquidationPriority(address collateralType) external {
ProposedRemoveCollateral.Data storage data = ProposedRemoveCollateral.load();
data.removeCollateralFromLiquidationPriority(collateralType);
}
}
contract ImplementationGasTest is Base_Test {
CurrentImplementation currentImplementation = new CurrentImplementation();
ProposedImplementation proposedImplementation = new ProposedImplementation();
uint256 private constant NUMBER_OF_COLLATERAL = 100;
uint256 private constant COLLATERAL_INDEX_TO_REMOVE = 42;
address[] private collateralTypes = new address[](NUMBER_OF_COLLATERAL);
function setUp() public override {
for (uint256 i = 0; i < NUMBER_OF_COLLATERAL; i++) {
collateralTypes[i] = address(uint160(i + 1));
}
}
function test_CurrentVsProposedImplementation() external {
console.log("Element to remove: %s", collateralTypes[COLLATERAL_INDEX_TO_REMOVE]);
vm.startPrank({ msgSender: users.owner.account });
currentImplementation.configureCollateralLiquidationPriority(collateralTypes);
console.log(
"Number of collaterals configured before calling current implementation: %s",
currentImplementation.getCollateralLiquidationPriority().length
);
uint256 gasStart = gasleft();
currentImplementation.removeCollateralFromLiquidationPriority(collateralTypes[COLLATERAL_INDEX_TO_REMOVE]);
uint256 gasEnd = gasleft();
vm.stopPrank();
uint256 gasCurrentImplementation = gasStart - gasEnd;
console.log("Gas used for current Implementation:", gasCurrentImplementation);
console.log(
"Number of collaterals after calling current implementation: %s\n",
currentImplementation.getCollateralLiquidationPriority().length
);
vm.startPrank({ msgSender: users.owner.account });
proposedImplementation.configureCollateralLiquidationPriority(collateralTypes);
console.log(
"Number of collaterals configured before calling proposed implementation: %s",
proposedImplementation.getCollateralLiquidationPriority().length
);
gasStart = gasleft();
proposedImplementation.removeCollateralFromLiquidationPriority(collateralTypes[COLLATERAL_INDEX_TO_REMOVE]);
gasEnd = gasleft();
vm.stopPrank();
uint256 gasProposedImplementation = gasStart - gasEnd;
console.log("Gas used for proposed implementation:", gasProposedImplementation);
console.log(
"Number of collaterals after calling proposed implementation: %s\n",
proposedImplementation.getCollateralLiquidationPriority().length
);
console.log(
"Are the arrays the same? %s",
compareArrays(
currentImplementation.getCollateralLiquidationPriority(),
proposedImplementation.getCollateralLiquidationPriority()
)
);
}
function compareArrays(address[] memory array1, address[] memory array2) public pure returns (bool) {
if (array1.length != array2.length) {
return false;
}
for (uint256 i = 0; i < array1.length; i++) {
if (array1[i] != array2[i]) {
return false;
}
}
return true;
}
}