The SafEth
contract allows protocol owners to adjust weights assigned to each supported derivative. The implementation also includes a function to rebalance all the derivatives to accommodate current balances to new weights:
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L138-L155
function rebalanceToWeights() external onlyOwner {
uint256 ethAmountBefore = address(this).balance;
for (uint i = 0; i < derivativeCount; i++) {
if (derivatives[i].balance() > 0)
derivatives[i].withdraw(derivatives[i].balance());
}
uint256 ethAmountAfter = address(this).balance;
uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore;
for (uint i = 0; i < derivativeCount; i++) {
if (weights[i] == 0 || ethAmountToRebalance == 0) continue;
uint256 ethAmount = (ethAmountToRebalance * weights[i]) /
totalWeight;
// Price will change due to slippage
derivatives[i].deposit{value: ethAmount}();
}
emit Rebalanced();
}
As we can see in the snippet, the rebalanceToWeights
function will withdraw all balance from each derivative to exchange them for ETH and then re-deposit everything again. This is highly inefficient and will result in losses, since swap fees and protocol fees will be paid for all the balance that each derivative holds.
In the following test we simulate a rebalancing of 200 ETH. After the unstake operation the user is left with 198.90 ETH, which implies a difference of a bit more than 0.5% (the numbers are taken by forking mainnet at block height 16906254).
Note: the snippet shows only the relevant code for the test. Full test file can be found here.
// Run this test forking mainnet at block height 16906254
function test_SafEth_rebalanceToWeights_InefficientImplementation() public {
// Setup derivatives
vm.prank(deployer);
safEth.addDerivative(address(wstEth), 1e18);
vm.prank(deployer);
safEth.addDerivative(address(reth), 1e18);
vm.prank(deployer);
safEth.addDerivative(address(sfrxEth), 1e18);
// user has 200 ether
uint256 initialAmount = 200 ether;
vm.deal(user, initialAmount);
// user stakes ether
vm.prank(user);
safEth.stake{value: initialAmount}();
// protocol owner changes weight and rebalances
vm.prank(deployer);
safEth.adjustWeight(0, 2e18);
vm.prank(deployer);
safEth.rebalanceToWeights();
// user unstakes
uint256 userShares = safEth.balanceOf(user);
vm.prank(user);
safEth.unstake(userShares);
// Balance is 198.902140399883614284 - A bit more than 0.5% is lost
uint256 balanceAfter = user.balance;
console.log(balanceAfter);
}
The rebalance operation doesn't need to withdraw all balance to ETH and convert it again. New weights can be accommodated by taking the difference of the current position and the expected position dictated by the new weights and just rebalancing that portion. More elaborate strategies can also be devised, since underlying assets from derivatives can be directly swapped for other assets instead of swapping them for ETH only to restake them in the other protocol.