The withdraw procedure for the SfrxEth
derivative involves first redeeming the sfrxETH for frxETH from the ERC4626 vault and then exchanging the frxETH for ETH using the Curve pool.
60: function withdraw(uint256 _amount) external onlyOwner {
61: IsFrxEth(SFRX_ETH_ADDRESS).redeem(
62: _amount,
63: address(this),
64: address(this)
65: );
66: uint256 frxEthBalance = IERC20(FRX_ETH_ADDRESS).balanceOf(
67: address(this)
68: );
69: IsFrxEth(FRX_ETH_ADDRESS).approve(
70: FRX_ETH_CRV_POOL_ADDRESS,
71: frxEthBalance
72: );
73:
74: uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
75: (10 ** 18 - maxSlippage)) / 10 ** 18;
76:
77: IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
78: 1,
79: 0,
80: frxEthBalance,
81: minOut
82: );
83: // solhint-disable-next-line
84: (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
85: ""
86: );
87: require(sent, "Failed to send Ether");
88: }
In line 61 the sfrxETH is redeemed for frxETH and the swap happens in line 77. For the exchange, the expected minimum amount is calculated in line 74 using the ethPerDerivative
function. As we can see in the following snippet, this function is dependent on the state of the sfrxETH vault:
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets(
10 ** 18
);
return ((10 ** 18 * frxAmount) /
IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
}
ethPerDerivative
uses the function convertToAssets
to calculate the amount of assets that equal one unit (1e18) of shares. The main issue is that the ERC4626 vault has been previously altered in the call to redeem: the first action in the withdraw
function already burned the shares in exchange for the assets (frxETH), which modified the total supply of assets and shares in the vault.
This means that using ethPerDerivative
to calculate the minimum output amount in the swap for the original withdrawal amount is inaccurate, since ethPerDerivative
uses the state of the vault which was already altered by the call to redeem.
The withdraw
function in the SfrxEth derivative should calculate the minimum output amount before redeeming the shares.
function withdraw(uint256 _amount) external onlyOwner {
uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
(10 ** 18 - maxSlippage)) / 10 ** 18;
IsFrxEth(SFRX_ETH_ADDRESS).redeem(
_amount,
address(this),
address(this)
);
uint256 frxEthBalance = IERC20(FRX_ETH_ADDRESS).balanceOf(
address(this)
);
IsFrxEth(FRX_ETH_ADDRESS).approve(
FRX_ETH_CRV_POOL_ADDRESS,
frxEthBalance
);
IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
1,
0,
frxEthBalance,
minOut
);
// solhint-disable-next-line
(bool sent, ) = address(msg.sender).call{value: address(this).balance}(
""
);
require(sent, "Failed to send Ether");
}