Skip to content

Commit

Permalink
Avalanche root gauge factory v2 (#2540)
Browse files Browse the repository at this point in the history
  • Loading branch information
jubeira authored Aug 10, 2023
1 parent 75cd38e commit 2276839
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 263 deletions.

This file was deleted.

9 changes: 9 additions & 0 deletions pkg/interfaces/contracts/liquidity-mining/IStakelessGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ pragma solidity >=0.7.0 <0.9.0;
import "./ILiquidityGauge.sol";

interface IStakelessGauge is ILiquidityGauge {
/// @dev Performs a checkpoint, computing how much should be minted for the gauge.
function checkpoint() external payable returns (bool);

/// @dev Returns the address that will receive the incentives (either the L2 gauge, or a mainnet address).
function getRecipient() external view returns (address);

/**
* @dev Returns total ETH bridge cost (post mint action) in wei.
* Each `checkpoint` should receive this exact amount to work. Some stakeless gauges don't actually need ETH
* to work; in those cases the cost will be 0.
*/
function getTotalBridgeCost() external view returns (uint256);
}
5 changes: 5 additions & 0 deletions pkg/liquidity-mining/contracts/gauges/StakelessGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ abstract contract StakelessGauge is IStakelessGauge, ReentrancyGuard {
return true;
}

/// @inheritdoc IStakelessGauge
function getTotalBridgeCost() external view virtual override returns (uint256) {
return 0;
}

function _currentPeriod() internal view returns (uint256) {
// solhint-disable-next-line not-rely-on-time
return (block.timestamp / 1 weeks) - 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ contract ArbitrumRootGauge is StakelessGauge {
);
}

function getTotalBridgeCost() external view returns (uint256) {
function getTotalBridgeCost() external view override returns (uint256) {
(uint256 gasLimit, uint256 gasPrice, uint256 maxSubmissionCost) = _factory.getArbitrumFees();
return _getTotalBridgeCost(gasLimit, gasPrice, maxSubmissionCost);
}
Expand Down
227 changes: 159 additions & 68 deletions pkg/liquidity-mining/contracts/gauges/avalanche/AvalancheRootGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,55 +13,104 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;

import "@balancer-labs/v2-interfaces/contracts/liquidity-mining/IAvalancheBridgeLimitsProvider.sol";

import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ERC20.sol";
import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol";
import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol";

import "./AvalancheRootGaugeLib.sol";
import "../StakelessGauge.sol";

/**
* @dev Initiate an outgoing bridge transaction.
*/
interface IMultichainV4Router {
function anySwapOutUnderlying(
address token,
address to,
uint256 amount,
uint256 toChainID
) external;
}
/// @dev Partial interface for LayerZero BAL proxy.
interface ILayerZeroBALProxy {
struct LzCallParams {
address payable refundAddress;
address zroPaymentAddress;
bytes adapterParams;
}

/**
* @dev Tokens to be bridged have AnySwap wrappers. This is necessary because some functions required by the bridge
* (e.g., `burn`) might not be exposed by the native token contracts.
*/
interface IAnyswapV6ERC20 is IERC20 {
function underlying() external returns (address);
/// @dev Returns packet type to be used in adapter params. It is a constant set to 0.
// solhint-disable-next-line func-name-mixedcase
function PT_SEND() external pure returns (uint8);

/// @dev Returns minimum gas limit required for the target `chainId` and `packetType`.
function minDstGasLookup(uint16 chainId, uint16 packetType) external view returns (uint256);

/// @dev Returns true if custom adapter parameters are activated in the proxy.
function useCustomAdapterParams() external view returns (bool);

/// @dev Returns the address of the underlying ERC20 token.
function token() external view returns (address);

/**
* @dev Estimate fee for sending token `_tokenId` to (`_dstChainId`, `_toAddress`).
* @param _dstChainId L0 defined chain id to send tokens to.
* @param _toAddress dynamic bytes array with the address you are sending tokens to on dstChain.
* @param _amount amount of the tokens to transfer.
* @param _useZro indicates to use zro to pay L0 fees.
* @param _adapterParams flexible bytes array to indicate messaging adapter services in L0.
*/
function estimateSendFee(
uint16 _dstChainId,
bytes32 _toAddress,
uint256 _amount,
bool _useZro,
bytes calldata _adapterParams
) external view returns (uint256 nativeFee, uint256 zroFee);

/**
* @dev Send `_amount` amount of token to (`_dstChainId`, `_toAddress`) from `_from`.
* @param _from the token owner.
* @param _dstChainId the destination chain identifier.
* @param _toAddress can be any size depending on the `dstChainId`.
* @param _amount the quantity of tokens in wei.
* @param _minAmount the minimum amount of tokens to receive on dstChain.
* @param _callParams struct with custom options.
* - refundAddress: the address LayerZero refunds if too much message fee is sent.
* - zroPaymentAddress set to address(0x0) if not paying in ZRO (LayerZero Token).
* - adapterParams is a flexible bytes array used to configure messaging adapter services.
*/
function sendFrom(
address _from,
uint16 _dstChainId,
bytes32 _toAddress,
uint256 _amount,
uint256 _minAmount,
LzCallParams calldata _callParams
) external payable;

/// @dev Returns the maximum allowed precision (decimals) for proxy transfers.
function sharedDecimals() external returns (uint8);
}

/**
* @notice Root Gauge for the Avalanche network.
* @dev Uses the multichain bridge. This stores a reference to the factory, which implements
* `IAvalancheBridgeLimitsProvider`, so the deployer must be the factory (or at least implement this interface).
*
* See general bridge docs here: https://docs.multichain.org/getting-started/how-it-works/cross-chain-router
* @dev Uses LayerZero OFTv2 (Omni Fungible Token V2) proxy contracts to bridge BAL.
* See https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oftv2 for reference.
*/
contract AvalancheRootGauge is StakelessGauge {
using SafeERC20 for IERC20;
using FixedPoint for uint256;

uint256 private constant _AVALANCHE_CHAIN_ID = 43114;
// LayerZero uses proprietary chain IDs.
// https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids#avalanche
uint16 private constant _AVALANCHE_LZ_CHAIN_ID = 106;

IAnyswapV6ERC20 private constant _ANYSWAP_BAL_WRAPPER = IAnyswapV6ERC20(0xcb9d0b8CfD8371143ba5A794c7218D4766c493e2);
// PT_SEND constant in proxy; replicated here for simplicity.
// See https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling/wire-up-configuration.
// and https://github.com/LayerZero-Labs/solidity-examples/blob/9134640fe5b618a047f365555e760c8736ebc162/contracts/token/oft/v2/OFTCoreV2.sol#L17.
// solhint-disable-previous-line max-line-length
uint16 private constant _SEND_PACKET_TYPE = 0;

IMainnetBalancerMinter private immutable _minter;
IMultichainV4Router private immutable _multichainRouter;
// https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters
uint16 private constant _ADAPTER_PARAMS_VERSION = 1;

// The bridge limits are set in the factory on deployment, and can be changed through a
// permissioned function defined there.
IAvalancheBridgeLimitsProvider private immutable _bridgeLimitsProvider;
ILayerZeroBALProxy private immutable _lzBALProxy;

// The proxy will truncate the amounts to send using this value, as it does not support 18 decimals.
// Any amount to send is truncated to this number, which depends on the shared decimals in the proxy.
// See https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1-vs-oftv2-which-should-i-use#what-are-the-differences-between-the-two-versions
// solhint-disable-previous-line max-line-length
uint256 private immutable _minimumBridgeAmount;

// This value is kept in storage and not made immutable to allow for this contract to be proxyable
address private _recipient;
Expand All @@ -70,66 +119,108 @@ contract AvalancheRootGauge is StakelessGauge {
* @dev Must be deployed by the AvalancheRootGaugeFactory, or other contract that implements
* `IAvalancheBridgeLimitsProvider`.
*/
constructor(IMainnetBalancerMinter minter, IMultichainV4Router multichainRouter) StakelessGauge(minter) {
_minter = minter;
_multichainRouter = multichainRouter;
_bridgeLimitsProvider = IAvalancheBridgeLimitsProvider(msg.sender);
constructor(IMainnetBalancerMinter minter, ILayerZeroBALProxy lzBALProxy) StakelessGauge(minter) {
_lzBALProxy = lzBALProxy;
uint8 decimalDifference = ERC20(address(minter.getBalancerToken())).decimals() - lzBALProxy.sharedDecimals();
_minimumBridgeAmount = 10**decimalDifference;
}

function initialize(address recipient, uint256 relativeWeightCap) external {
// Sanity check that the underlying token of the minter is the same we've wrapped for Avalanche.
require(_ANYSWAP_BAL_WRAPPER.underlying() == address(_minter.getBalancerToken()), "Invalid Wrapper Token");
require(_lzBALProxy.token() == address(_balToken), "Invalid Wrapper Token");

// This will revert in all calls except the first one
__StakelessGauge_init(relativeWeightCap);

_recipient = recipient;
}

/**
* @dev The address of the L2 recipient gauge.
*/
function getRecipient() external view override returns (address) {
/// @inheritdoc IStakelessGauge
function getRecipient() public view override returns (address) {
return _recipient;
}

/**
* @dev Return the Multichain Router contract used to bridge.
*/
function getMultichainRouter() external view returns (IMultichainV4Router) {
return _multichainRouter;
/// @dev Return the Layer Zero proxy contract for the underlying BAL token.
function getBALProxy() external view returns (address) {
return address(_lzBALProxy);
}

/**
* @dev Return the AnySwap wrapper for the underlying BAL token.
* @dev Returns the minimum amount of tokens that can be bridged.
* Values lower than this one will not even be transferred to the proxy.
*/
function getAnyBAL() external pure returns (IERC20) {
return IERC20(_ANYSWAP_BAL_WRAPPER);
function getMinimumBridgeAmount() public view returns (uint256) {
return _minimumBridgeAmount;
}

/// @inheritdoc IStakelessGauge
function getTotalBridgeCost() public view override returns (uint256) {
return _getTotalBridgeCost(_getAdapterParams());
}

function _getTotalBridgeCost(bytes memory adapterParams) internal view returns (uint256) {
// Estimate fee does not depend on the amount to bridge.
// We just set it to 0 so that we can have the same external interface across other gauges that require ETH.
(uint256 nativeFee, ) = _lzBALProxy.estimateSendFee(
_AVALANCHE_LZ_CHAIN_ID,
AvalancheRootGaugeLib.bytes32Recipient(getRecipient()),
0,
false,
adapterParams
);

return nativeFee;
}

function _postMintAction(uint256 mintAmount) internal override {
(uint256 minBridgeAmount, uint256 maxBridgeAmount) = _bridgeLimitsProvider.getAvalancheBridgeLimits();

// This bridge extracts a fee in the token being transferred.
// It is 0.1%, but subject to a minimum and a maximum, so it can be quite significant for small amounts
// (e.g., around 50% if you transfer the current minimum of ~1.5 BAL).
//
// The bridge operation will fail in a silent and deadly manner if the amount bounds are exceeded -
// the transaction will succeed, but the tokens will be locked forever in the AnySwap wrapper - so validate
// the amounts first before attempting to bridge.
require(mintAmount >= minBridgeAmount, "Below Bridge Limit");
require(mintAmount <= maxBridgeAmount, "Above Bridge Limit");
uint256 amountWithoutDust = AvalancheRootGaugeLib.removeDust(mintAmount, _minimumBridgeAmount);
// If there is nothing to bridge, we return early.
if (amountWithoutDust == 0) {
return;
}

// The underlying token will be transferred, and must be approved.
_balToken.safeApprove(address(_multichainRouter), mintAmount);
bytes memory adapterParams = _getAdapterParams();
uint256 totalBridgeCost = _getTotalBridgeCost(adapterParams);

// Progress and results can be monitored using the multichain scanner:
// https://scan.multichain.org/#/tx?params=<mainnet txid>
_multichainRouter.anySwapOutUnderlying(
address(_ANYSWAP_BAL_WRAPPER),
_recipient,
require(msg.value == totalBridgeCost, "Incorrect msg.value passed");

// The underlying token will be transferred, and must be approved.
_balToken.safeApprove(address(_lzBALProxy), mintAmount);

// Progress and results can be monitored using the Layer Zero scanner: https://layerzeroscan.com/
// The BAL proxy uses less than 18 decimals, so any amount with greater precision than the supported one will
// be truncated.
// This is why we remove "dust" the same way the proxy does to provide an appropriate minimum amount and
// ensure the transfer does not revert.
// This assumes that there is no fee for the token, neither in the proxy (which can be set by governance, but
// it is not expected to happen ever), nor for the token transfer itself (the BAL token does not take a cut
// in `transferFrom`, so it is OK).
_lzBALProxy.sendFrom{ value: totalBridgeCost }(
address(this),
_AVALANCHE_LZ_CHAIN_ID,
AvalancheRootGaugeLib.bytes32Recipient(getRecipient()),
mintAmount,
_AVALANCHE_CHAIN_ID
amountWithoutDust,
ILayerZeroBALProxy.LzCallParams(payable(msg.sender), address(0), adapterParams)
);
}

function _getAdapterParams() internal view returns (bytes memory) {
// Adapter params should either encode the minimum destination gas if custom parameters are used, or be
// an empty bytes array otherwise.
// See https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters
// These lines were reverse-engineered from the BAL proxy and its dependencies (LZ endpoint and relayer).

// solhint-disable max-line-length
// See https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/RelayerV2.sol#L104-L112
// https://github.com/LayerZero-Labs/solidity-examples/blob/8e00603ae03995622d643722d6d194f830774208/contracts/token/oft/v2/OFTCoreV2.sol#L178-L179
// https://github.com/LayerZero-Labs/solidity-examples/blob/8e00603ae03995622d643722d6d194f830774208/contracts/lzApp/LzApp.sol#L57-L58
// solhint-enable max-line-length
if (_lzBALProxy.useCustomAdapterParams()) {
uint256 minDstGas = _lzBALProxy.minDstGasLookup(_AVALANCHE_LZ_CHAIN_ID, _SEND_PACKET_TYPE);
return abi.encodePacked(_ADAPTER_PARAMS_VERSION, minDstGas);
} else {
return bytes("");
}
}
}
Loading

0 comments on commit 2276839

Please sign in to comment.