Skip to content

Commit

Permalink
Merge pull request #297 from asymmetryfinance/sfrx-eth-adapt
Browse files Browse the repository at this point in the history
♻️ Redeploy
  • Loading branch information
Philogy authored Feb 13, 2024
2 parents 21cf938 + ece7670 commit ae2ded9
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 86 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ technical details on internal works view ["AfEth"](./docs/AfEth.md).

|Description|Address + Link|
|-----------|--------------|
|The AfEth ERC1967 proxy, deployed using Solady's [ERC1967 Factory](https://etherscan.io/address/0x0000000000006396FF2a80c067f99B3d2Ab4Df24#readContract).| [`0x000000007896ae1058cb6bbd9d472c2c9aade11e`](https://etherscan.io/address/0x000000007896ae1058cb6bbd9d472c2c9aade11e)|
|The Votium ERC1967 proxy, also deployed using Solady's [ERC1967 Factory](https://etherscan.io/address/0x0000000000006396FF2a80c067f99B3d2Ab4Df24#readContract)|[`0x000000000F62A9e2f167c19a7010Fadd733b43B4`](https://etherscan.io/address/0x000000000F62A9e2f167c19a7010Fadd733b43B4)|
|Votium Implementation Contract|[`0x1bc24b2755c812996bae34f6fab19cd68f5cff34`](https://etherscan.io/address/0xcf687792a1e65bb41793cc938cf8e27e5d1b678b)|
|AfEth Implementation Contract|[`0x61b0d41dd07247c1c5a7f93ff2aef21ab66103f3`](https://etherscan.io/address/0x1a0a62aa8a9471a6a726b5cdc24192be3a3dbc7b)|

|The AfEth ERC1967 proxy, deployed using Solady's [ERC1967 Factory](https://etherscan.io/address/0x0000000000006396FF2a80c067f99B3d2Ab4Df24#readContract).| [`0x00000000fbAA96B36A2AcD4B7B36385c426B119D`](https://etherscan.io/address/0x00000000fbAA96B36A2AcD4B7B36385c426B119D)|
|The Votium ERC1967 proxy, also deployed using Solady's [ERC1967 Factory](https://etherscan.io/address/0x0000000000006396FF2a80c067f99B3d2Ab4Df24#readContract)|[`0x000000004c4bb6e0a169FE3A9228cd0F70873CdE`](https://etherscan.io/address/0x000000004c4bb6e0a169FE3A9228cd0F70873CdE)|
|Votium Implementation Contract|[`0xCf687792a1e65bB41793cc938Cf8e27E5D1B678b`](https://etherscan.io/address/0xCf687792a1e65bB41793cc938Cf8e27E5D1B678b)|
|AfEth Implementation Contract|[`0x1a0A62AA8A9471a6a726B5cDc24192Be3a3DBc7B`](https://etherscan.io/address/0x1a0A62AA8A9471a6a726B5cDc24192Be3a3DBc7B)|
|AfEthRelayer, also deployed using Solady's [ERC1967 Factory](https://etherscan.io/address/0x0000000000006396FF2a80c067f99B3d2Ab4Df24#readContract)|[`0x00000000b8791985c4bd2cbc4584cee89c4e95ef`](https://etherscan.io/address/0x00000000b8791985c4bd2cbc4584cee89c4e95ef)|

38 changes: 36 additions & 2 deletions docs/Operating-Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,46 @@ permissionless actions that are accessible by anyone.
> this will pause all deposit/withdrawal functions as well as disable the votium strategy, note that
> this method may consume quite a bit of gas.
### Reward Claiming & Rebalancing

In the following steps `Votium` is short for the `VotiumStrategy` contract.

1. As the rewarder: Claim the rewards using the `Votium::claimRewards(IVotiumMerkleStash.ClaimParam[] claimProofs)` method
2. (One-time setup) As the owner: Grant allowances on behalf of the Votium contract to addresses
you'd like to use to swap through by calling `Votium::grantAddedAllowances(Allowance[] allowances)`
3. As the rewarder: Swap the rewards using the `Votium::swapRewards(Swap[] swaps, uint256 cvxPerEthMin, uint256 sfrxPerEthMin, uint256 ethPerSfrxMin, uint256 deadline)` be sure to provide accurate values for `cvxPerEthMin`, `sfrxPerEthMin` and `ethPerSfrxMin` as they'll set the slippage for the rebalance / reward distribution callback.
4. (Additionally) As the rewarder / owner: Call `AfEth::depositRewardsAndRebalance(IAfEth.RebalanceParams params)` at set intervals to ensure the trickle unlocked rewards are swapped for CVX & sfrxETH and are available for actual withdrawal (Note: Not entirely necessary as `swapRewards` also triggers this).

### Managing the Quick Action Reserves

These calls are only available to the owner. Note that fees will accrue directly to the quick action
reserves. You can immediately withdraw them after triggering a rebalance / reward distribution via
`swapRewards` or `depositRewardsAndRebalance` to avoid this.

- Deposit afETH + ETH: `AfEth::depositForQuickActions(uint256 afEthAmount) payable`. Note you can
set `afEthAmount` to `1 << 255` to indicate you'd like to deposit the owner's entire afETH balance.
- Withdraw afETH + ETH: `AfEth::withdrawOwnerFunds(uint256 afEthAmount, uint256 ethAmount)`. Similar
to the deposit for quick actions methods you can specify `1 << 255` for either input to indicate
you'd like to withdraw everything.

### Who?

- Owner (`AfEth::owner()`, `Votium::owner()`)
- Owner (`AfEth::owner()`, `Votium::owner()`): Is allow

### Configuring Roles

**Changing the owner (AfEth & Votium)**

1. The new owner needs to call `requestOwnershipHandover()` this will request a handover.
2. The current owner will then have 48h to call `completeOwnershipHandover(address newOwner)` to
confirm the handover.

This process is recommended as it ensures you don't accidentally transfer ownership to a wallet you
don't have control over.

### Configuring Parameters
**Changing the rewarder (AfEth & Votium)**

As the owner call `setRewarder(address)` with the address of the new rewarder.

## User

Expand Down
17 changes: 4 additions & 13 deletions script/DeployMainnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,17 @@ import {console2 as console} from "forge-std/console2.sol";

/// @author philogy <https://github.com/philogy>
contract DeployMainnetScript is Test, Script {
bytes32 internal constant AF_ETH_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbec654a8355b49778011000c0;
bytes32 internal constant VOTIUM_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbec654a8355b43c2415010040;
bytes32 internal constant AF_ETH_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbec654a8355b46947ec1a0010;
bytes32 internal constant VOTIUM_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbf13066ae7fad3ff30a060010;

function run() public {
ERC1967Factory factory = ERC1967Factory(ERC1967FactoryConstants.ADDRESS);

// address OWNER = 0x263b03BbA0BbbC320928B6026f5eAAFAD9F1ddeb;
// address REWARDER = 0xa927c81CC214cc991613cB695751Bc932F042501;
address OWNER = vm.addr(vm.envUint("TEST1_KEY"));
address REWARDER = OWNER;
address OWNER = 0x263b03BbA0BbbC320928B6026f5eAAFAD9F1ddeb;
address REWARDER = 0xa927c81CC214cc991613cB695751Bc932F042501;

// TODO: Not the recommended way of loading private key.
uint256 pk = vm.envUint("PRIV_KEY");
address me = vm.addr(pk);

// TESTING: Grant deployer ETH
// vm.startBroadcast(vm.envUint("TEST0_KEY"));
// (bool success,) = me.call{value: 9999 ether}("");
// success;
// vm.stopBroadcast();

vm.startBroadcast(pk);

Expand Down
42 changes: 42 additions & 0 deletions script/DeployRelay.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {Script} from "forge-std/Script.sol";
import {Test} from "forge-std/Test.sol";
import {ERC1967FactoryConstants} from "solady/src/utils/ERC1967FactoryConstants.sol";
import {ERC1967Factory} from "solady/src/utils/ERC1967Factory.sol";
import {AfEthRelayer} from "../src/AfEthRelayer.sol";
import {console2 as console} from "forge-std/console2.sol";

/// @author philogy <https://github.com/philogy>
contract DeployRelayScript is Test, Script {
bytes32 internal constant AF_ETH_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbec654a8355b46947ec1a0010;
bytes32 internal constant VOTIUM_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbf13066ae7fad3ff30a060010;

bytes32 internal constant RELAY_VANITY_SALT = 0x67b80ff33e5937b58b2a46870a912cb11d231efbbab440b5457a1800089dc6c5;

function run() public {
ERC1967Factory factory = ERC1967Factory(ERC1967FactoryConstants.ADDRESS);

address OWNER = 0x263b03BbA0BbbC320928B6026f5eAAFAD9F1ddeb;

// TODO: Not the recommended way of loading private key.
uint256 pk = vm.envUint("PRIV_KEY");

vm.startBroadcast(pk);

address relayImplementation = address(new AfEthRelayer());

address relay = factory.deployDeterministicAndCall(
relayImplementation,
OWNER,
RELAY_VANITY_SALT,
abi.encodeCall(AfEthRelayer.initialize, ())
);


console.log("relay successfuly deployed at %s", relay);

vm.stopBroadcast();
}
}
1 change: 0 additions & 1 deletion script/SetupQuick.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ contract SetupQuickScript is Test, Script {

function run() public {
uint256 pk = vm.envUint("TEST1_KEY");
address me = vm.addr(pk);

vm.startBroadcast(pk);

Expand Down
123 changes: 59 additions & 64 deletions src/AfEthRelayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,33 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol";
import {IAfEth} from "./interfaces/afeth/IAfEth.sol";
import {ISafEth} from "./interfaces/safeth/ISafEth.sol";
import {WETH} from "./interfaces/IWETH.sol";
import {IWETH, WETH} from "./interfaces/IWETH.sol";

// AfEth is the strategy manager for safEth and votium strategies
contract AfEthRelayer is Initializable {
using SafeTransferLib for address;

ISafEth public constant SAF_ETH = ISafEth(0x6732Efaf6f39926346BeF8b821a04B6361C4F3e5);
IAfEth public immutable AF_ETH;
IAfEth public constant AF_ETH = IAfEth(0x00000000fbAA96B36A2AcD4B7B36385c426B119D);

address internal immutable THIS_ = address(this);

address internal constant ZERO_X_EXCHANGE = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF;
address internal constant ZERO_X_ERC20_PROXY = 0x95E6F48254609A6ee006F7D493c8e5fB97094ceF;

error NotWhitelisted();
struct SwapParams {
address sellToken;
uint256 amount;
bytes swapCallData;
}

error InnerCallFailed();
error SwapFailed();

// As recommended by https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address afEth) {
constructor() {
_disableInitializers();
AF_ETH = IAfEth(afEth);
}

// Payable fallback to allow this contract to receive protocol fee refunds.
Expand All @@ -37,76 +44,64 @@ contract AfEthRelayer is Initializable {
function initialize() external initializer {}

/**
* @notice - Deposits into the SafEth contract and relay to owner address
* @param minOut - Minimum amount of SafEth to mint
* @param to - Owner of the SafEth
* @notice Deposits into the SafEth contract and relay to owner address
* @param minOut Minimum amount of safETH to mint
* @param params Parameters passed to zerox
*/
function depositSafEth(
uint256 minOut,
address to,
address sellToken,
uint256 sellAmount,
address allowanceTarget,
address swapTarget,
bytes calldata swapData
) external {
uint256 balanceBefore = WETH.balanceOf(address(this));
fillQuote(sellToken, sellAmount, allowanceTarget, swapTarget, swapData);
uint256 balanceAfter = WETH.balanceOf(address(this));
uint256 amountToStake = balanceAfter - balanceBefore;
WETH.withdraw(amountToStake);

uint256 amountToTransfer = SAF_ETH.stake{value: amountToStake}(minOut);
address(SAF_ETH).safeTransfer(to, amountToTransfer);
function depositSafEth(uint256 minOut, SwapParams calldata params) external payable {
_swapToEth(params);

uint256 amountToTransfer = SAF_ETH.stake{value: address(this).balance}(minOut);
address(SAF_ETH).safeTransfer(msg.sender, amountToTransfer);
}

/**
* @notice - Deposits into the AfEth contract and relay to owner address
* @param minOut - Minimum amount of AfEth to mint
* @param deadline - Time before transaction expires
* @param _owner - Owner of the AfEth
* @notice Does a direct deposit into the AfEth contract and relay to caller
* @param minOut Minimum amount of afETH to mint
* @param deadline Time before transaction expires
* @param params Owner of the AfEth
*/
function depositAfEth(
uint256 minOut,
uint256 deadline,
address _owner,
address _sellToken,
uint256 _amount,
address _allowanceTarget,
address payable _to,
bytes calldata _swapCallData
) external {
uint256 balanceBefore = WETH.balanceOf(address(this));
fillQuote(_sellToken, _amount, _allowanceTarget, _to, _swapCallData);
uint256 balanceAfter = WETH.balanceOf(address(this));
uint256 amountToStake = balanceAfter - balanceBefore;

WETH.withdraw(amountToStake);

AF_ETH.deposit{value: amountToStake}(_owner, minOut, deadline);
function depositAfEth(uint256 minOut, uint256 deadline, SwapParams calldata params) external payable {
_swapToEth(params);
AF_ETH.deposit{value: address(this).balance}(msg.sender, minOut, deadline);
}

function whitelisted(address addr) public pure returns (bool) {
return addr == ZERO_X_EXCHANGE || addr == ZERO_X_ERC20_PROXY;
/**
* @notice Does a quick deposit into the AfEth contract and relay to caller
* @param minOut Minimum amount of afETH to mint
* @param deadline Time before transaction expires
* @param params Owner of the AfEth
*/
function quickDepositAfEth(uint256 minOut, uint256 deadline, SwapParams calldata params) external payable {
_swapToEth(params);
AF_ETH.quickDeposit{value: address(this).balance}(msg.sender, minOut, deadline);
}

/// @dev Swaps ERC20->ERC20 tokens held by this contract using a 0x-API quote.
function fillQuote(
address sellToken,
uint256 amount,
address spender,
address swapTarget,
bytes calldata swapCallData
) private {
if (!whitelisted(swapTarget) || !whitelisted(spender)) {
revert NotWhitelisted();
/**
* @dev Enables a new sell token by doing the one-time infinite approval from the relayer,
* allows for an optional nested call to an actual deposit transaction.
*/
function enableNewSellToken(address sellToken, bytes calldata innerCall) external payable {
sellToken.safeApproveWithRetry(ZERO_X_ERC20_PROXY, type(uint256).max);
if (innerCall.length > 0) {
// Delegate to `THIS_` (implementation address) to avoid proxy overhead, functionally
// equivalent to calling address(this).
(bool success,) = THIS_.delegatecall(innerCall);
if (!success) revert InnerCallFailed();
}
sellToken.safeTransferFrom(msg.sender, address(this), amount);
sellToken.safeApproveWithRetry(spender, amount);
}

function _swapToEth(SwapParams calldata params) internal {
_fillQuote(params);
uint256 totalBal = WETH.balanceOf(address(this));
IWETH(WETH).withdraw(totalBal);
}

/// @dev Swaps ERC20->ERC20 tokens held by this contract using a 0x-API quote.
function _fillQuote(SwapParams calldata params) private {
params.sellToken.safeTransferFrom(msg.sender, address(this), params.amount);

// Arbitrary call ok because `swapTarget` needs to be one of the hardcoded whitelisted
// addresses.
(bool success,) = swapTarget.call(swapCallData);
(bool success,) = ZERO_X_EXCHANGE.call(params.swapCallData);
if (!success) revert SwapFailed();
}
}
2 changes: 1 addition & 1 deletion src/interfaces/IWETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

IWETH constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

interface IWETH is IERC20 {
function deposit() external payable;
Expand Down

0 comments on commit ae2ded9

Please sign in to comment.