diff --git a/balancer-js/package.json b/balancer-js/package.json index 5c7d8bdda..48b10046f 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "0.1.23", + "version": "0.1.24", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk/balancer-js#readme", @@ -51,6 +51,7 @@ "@graphql-codegen/typescript-graphql-request": "^4.3.0", "@graphql-codegen/typescript-operations": "^2.2.0", "@graphql-codegen/typescript-resolvers": "2.4.1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.4", "@nomiclabs/hardhat-ethers": "^2.0.5", "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "^4.1.0", diff --git a/balancer-js/src/balancerErrors.ts b/balancer-js/src/balancerErrors.ts index d0cc194bc..94a59e489 100644 --- a/balancer-js/src/balancerErrors.ts +++ b/balancer-js/src/balancerErrors.ts @@ -9,6 +9,7 @@ export enum BalancerErrorCode { NO_POOL_DATA = 'NO_POOL_DATA', INPUT_OUT_OF_BOUNDS = 'INPUT_OUT_OF_BOUNDS', INPUT_LENGTH_MISMATCH = 'INPUT_LENGTH_MISMATCH', + INPUT_ZERO_NOT_ALLOWED = 'INPUT_ZERO_NOT_ALLOWED', TOKEN_MISMATCH = 'TOKEN_MISMATCH', MISSING_TOKENS = 'MISSING_TOKENS', MISSING_AMP = 'MISSING_AMP', @@ -57,6 +58,8 @@ export class BalancerError extends Error { return 'missing price rate'; case BalancerErrorCode.MISSING_WEIGHT: return 'missing weight'; + case BalancerErrorCode.INPUT_ZERO_NOT_ALLOWED: + return 'zero input not allowed'; default: return 'Unknown error'; } diff --git a/balancer-js/src/index.ts b/balancer-js/src/index.ts index 2ac3ad315..51936a946 100644 --- a/balancer-js/src/index.ts +++ b/balancer-js/src/index.ts @@ -1,5 +1,6 @@ export * from './pool-stable'; export * from './pool-weighted'; +export * from './pool-composable-stable'; export * from './pool-utils'; export * from './lib/utils'; export * from './types'; @@ -15,6 +16,7 @@ export * from './modules/sor/sor.module'; export * from './modules/pools'; export * from './modules/data'; export * from './balancerErrors'; +export * from './lib/utils/signatures'; export { SwapInfo, SubgraphPoolBase, diff --git a/balancer-js/src/lib/abi/BatchRelayerLibrary.json b/balancer-js/src/lib/abi/BatchRelayerLibrary.json new file mode 100644 index 000000000..ca1953a4c --- /dev/null +++ b/balancer-js/src/lib/abi/BatchRelayerLibrary.json @@ -0,0 +1,1011 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "wstETH", + "type": "address" + }, + { + "internalType": "contract IBalancerMinter", + "name": "minter", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approveVault", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + }, + { + "internalType": "int256[]", + "name": "limits", + "type": "int256[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "key", + "type": "uint256" + } + ], + "internalType": "struct VaultActions.OutputReference[]", + "name": "outputReferences", + "type": "tuple[]" + } + ], + "name": "batchSwap", + "outputs": [ + { + "internalType": "int256[]", + "name": "", + "type": "int256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum VaultActions.PoolKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.ExitPoolRequest", + "name": "request", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "key", + "type": "uint256" + } + ], + "internalType": "struct VaultActions.OutputReference[]", + "name": "outputReferences", + "type": "tuple[]" + } + ], + "name": "exitPool", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IStakingLiquidityGauge[]", + "name": "gauges", + "type": "address[]" + } + ], + "name": "gaugeClaimRewards", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IStakingLiquidityGauge", + "name": "gauge", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "gaugeDeposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "gauges", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "gaugeMint", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "approval", + "type": "bool" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "gaugeSetMinterApproval", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IStakingLiquidityGauge", + "name": "gauge", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "gaugeWithdraw", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getEntrypoint", + "outputs": [ + { + "internalType": "contract IBalancerRelayer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum VaultActions.PoolKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.JoinPoolRequest", + "name": "request", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "joinPool", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IVault.UserBalanceOpKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + } + ], + "internalType": "struct IVault.UserBalanceOp[]", + "name": "ops", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "manageUserBalance", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "authorisation", + "type": "bytes" + } + ], + "name": "setRelayerApproval", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "stakeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "stakeETHAndWrap", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "assetIn", + "type": "address" + }, + { + "internalType": "contract IAsset", + "name": "assetOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.SingleSwap", + "name": "singleSwap", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IStaticATokenLM", + "name": "staticToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "toUnderlying", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapAaveStaticToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapERC4626", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IUnbuttonToken", + "name": "wrapperToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapUnbuttonToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "unwrapWstETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20Permit", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "vaultPermit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20PermitDAI", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "holder", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "allowed", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "vaultPermitDAI", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IStaticATokenLM", + "name": "staticToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "fromUnderlying", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapAaveDynamicToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC4626", + "name": "wrappedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapERC4626", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapStETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IUnbuttonToken", + "name": "wrapperToken", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "uAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputReference", + "type": "uint256" + } + ], + "name": "wrapUnbuttonToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/balancer-js/src/lib/abi/VaultActions.json b/balancer-js/src/lib/abi/VaultActions.json deleted file mode 100644 index e8e007a1e..000000000 --- a/balancer-js/src/lib/abi/VaultActions.json +++ /dev/null @@ -1,428 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approveVault", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "enum IVault.SwapKind", - "name": "kind", - "type": "uint8" - }, - { - "components": [ - { - "internalType": "bytes32", - "name": "poolId", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "assetInIndex", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "assetOutIndex", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "internalType": "struct IVault.BatchSwapStep[]", - "name": "swaps", - "type": "tuple[]" - }, - { - "internalType": "contract IAsset[]", - "name": "assets", - "type": "address[]" - }, - { - "components": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "bool", - "name": "fromInternalBalance", - "type": "bool" - }, - { - "internalType": "address payable", - "name": "recipient", - "type": "address" - }, - { - "internalType": "bool", - "name": "toInternalBalance", - "type": "bool" - } - ], - "internalType": "struct IVault.FundManagement", - "name": "funds", - "type": "tuple" - }, - { - "internalType": "int256[]", - "name": "limits", - "type": "int256[]" - }, - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "components": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "key", - "type": "uint256" - } - ], - "internalType": "struct VaultActions.OutputReference[]", - "name": "outputReferences", - "type": "tuple[]" - } - ], - "name": "batchSwap", - "outputs": [ - { - "internalType": "int256[]", - "name": "", - "type": "int256[]" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "poolId", - "type": "bytes32" - }, - { - "internalType": "enum VaultActions.PoolKind", - "name": "kind", - "type": "uint8" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address payable", - "name": "recipient", - "type": "address" - }, - { - "components": [ - { - "internalType": "contract IAsset[]", - "name": "assets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "minAmountsOut", - "type": "uint256[]" - }, - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - }, - { - "internalType": "bool", - "name": "toInternalBalance", - "type": "bool" - } - ], - "internalType": "struct IVault.ExitPoolRequest", - "name": "request", - "type": "tuple" - }, - { - "components": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "key", - "type": "uint256" - } - ], - "internalType": "struct VaultActions.OutputReference[]", - "name": "outputReferences", - "type": "tuple[]" - } - ], - "name": "exitPool", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "getVault", - "outputs": [ - { - "internalType": "contract IVault", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "poolId", - "type": "bytes32" - }, - { - "internalType": "enum VaultActions.PoolKind", - "name": "kind", - "type": "uint8" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "components": [ - { - "internalType": "contract IAsset[]", - "name": "assets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "maxAmountsIn", - "type": "uint256[]" - }, - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - }, - { - "internalType": "bool", - "name": "fromInternalBalance", - "type": "bool" - } - ], - "internalType": "struct IVault.JoinPoolRequest", - "name": "request", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "outputReference", - "type": "uint256" - } - ], - "name": "joinPool", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "enum IVault.UserBalanceOpKind", - "name": "kind", - "type": "uint8" - }, - { - "internalType": "contract IAsset", - "name": "asset", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address payable", - "name": "recipient", - "type": "address" - } - ], - "internalType": "struct IVault.UserBalanceOp[]", - "name": "ops", - "type": "tuple[]" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "manageUserBalance", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bytes32", - "name": "poolId", - "type": "bytes32" - }, - { - "internalType": "enum IVault.SwapKind", - "name": "kind", - "type": "uint8" - }, - { - "internalType": "contract IAsset", - "name": "assetIn", - "type": "address" - }, - { - "internalType": "contract IAsset", - "name": "assetOut", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "userData", - "type": "bytes" - } - ], - "internalType": "struct IVault.SingleSwap", - "name": "singleSwap", - "type": "tuple" - }, - { - "components": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "bool", - "name": "fromInternalBalance", - "type": "bool" - }, - { - "internalType": "address payable", - "name": "recipient", - "type": "address" - }, - { - "internalType": "bool", - "name": "toInternalBalance", - "type": "bool" - } - ], - "internalType": "struct IVault.FundManagement", - "name": "funds", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "limit", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "outputReference", - "type": "uint256" - } - ], - "name": "swap", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - } -] diff --git a/balancer-js/src/modules/pools/index.ts b/balancer-js/src/modules/pools/index.ts index 2826c7601..ec14c5197 100644 --- a/balancer-js/src/modules/pools/index.ts +++ b/balancer-js/src/modules/pools/index.ts @@ -101,16 +101,21 @@ export class Pools implements Findable { slippage, shouldUnwrapNativeAsset = false, singleTokenMaxOut - ) => - methods.exit.buildExitExactBPTIn({ - exiter, - pool, - bptIn, - slippage, - shouldUnwrapNativeAsset, - wrappedNativeAsset, - singleTokenMaxOut, - }), + ) => { + if (methods.exit.buildExitExactBPTIn) { + return methods.exit.buildExitExactBPTIn({ + exiter, + pool, + bptIn, + slippage, + shouldUnwrapNativeAsset, + wrappedNativeAsset, + singleTokenMaxOut, + }); + } else { + throw 'ExitExactBPTIn not supported'; + } + }, buildExitExactTokensOut: (exiter, tokensOut, amountsOut, slippage) => methods.exit.buildExitExactTokensOut({ exiter, diff --git a/balancer-js/src/modules/pools/pool-type-concerns.ts b/balancer-js/src/modules/pools/pool-type-concerns.ts index 4406537ed..1a3ce13df 100644 --- a/balancer-js/src/modules/pools/pool-type-concerns.ts +++ b/balancer-js/src/modules/pools/pool-type-concerns.ts @@ -1,5 +1,6 @@ import { BalancerSdkConfig, PoolType } from '@/types'; import { Stable } from './pool-types/stable.module'; +import { ComposableStable } from './pool-types/composableStable.module'; import { Weighted } from './pool-types/weighted.module'; import { MetaStable } from './pool-types/metaStable.module'; import { StablePhantom } from './pool-types/stablePhantom.module'; @@ -16,6 +17,7 @@ export class PoolTypeConcerns { config: BalancerSdkConfig, public weighted = new Weighted(), public stable = new Stable(), + public composableStable = new ComposableStable(), public metaStable = new MetaStable(), public stablePhantom = new StablePhantom(), public linear = new Linear() @@ -23,7 +25,13 @@ export class PoolTypeConcerns { static from( poolType: PoolType - ): Weighted | Stable | MetaStable | StablePhantom | Linear { + ): + | Weighted + | Stable + | ComposableStable + | MetaStable + | StablePhantom + | Linear { // Calculate spot price using pool type switch (poolType) { case 'Weighted': @@ -34,6 +42,9 @@ export class PoolTypeConcerns { case 'Stable': { return new Stable(); } + case 'ComposableStable': { + return new ComposableStable(); + } case 'MetaStable': { return new MetaStable(); } diff --git a/balancer-js/src/modules/pools/pool-types/composableStable.module.ts b/balancer-js/src/modules/pools/pool-types/composableStable.module.ts new file mode 100644 index 000000000..e39949d83 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/composableStable.module.ts @@ -0,0 +1,23 @@ +import { StablePoolJoin } from './concerns/stable/join.concern'; +import { StablePoolLiquidity } from './concerns/stable/liquidity.concern'; +import { StablePoolSpotPrice } from './concerns/stable/spotPrice.concern'; +import { StablePoolPriceImpact } from './concerns/stable/priceImpact.concern'; +import { ComposableStablePoolExit } from './concerns/composableStable/exit.concern'; +import { PoolType } from './pool-type.interface'; +import { + ExitConcern, + JoinConcern, + LiquidityConcern, + PriceImpactConcern, + SpotPriceConcern, +} from './concerns/types'; + +export class ComposableStable implements PoolType { + constructor( + public exit: ExitConcern = new ComposableStablePoolExit(), + public join: JoinConcern = new StablePoolJoin(), + public liquidity: LiquidityConcern = new StablePoolLiquidity(), + public spotPriceCalculator: SpotPriceConcern = new StablePoolSpotPrice(), + public priceImpactCalculator: PriceImpactConcern = new StablePoolPriceImpact() + ) {} +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.spec.ts new file mode 100644 index 000000000..aff3b6ee5 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import { parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; + +import { BalancerSDK, Network, Pool } from '@/.'; +import pools_14717479 from '@/test/lib/pools_14717479.json'; +import { ComposableStablePoolExit } from './exit.concern'; + +const concern = new ComposableStablePoolExit(); + +const rpcUrl = ''; +const network = Network.MAINNET; +const { networkConfig } = new BalancerSDK({ network, rpcUrl }); +const wrappedNativeAsset = + networkConfig.addresses.tokens.wrappedNativeAsset.toLowerCase(); + +const pool = pools_14717479.find( + (pool) => + pool.id == + '0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063' // Balancer USD Stable Pool - staBAL3 +) as unknown as Pool; + +describe('exit module', () => { + describe('buildExitExactBPTIn', () => { + context('exit with ETH', () => { + it('should fail due to conflicting inputs', () => { + let errorMessage = ''; + try { + concern.buildExitSingleTokenOut({ + exiter: '0x35f5a330FD2F8e521ebd259FA272bA8069590741', + pool, + bptIn: parseFixed('10', 18).toString(), + slippage: '100', // 100 bps + shouldUnwrapNativeAsset: false, + wrappedNativeAsset, + singleTokenMaxOut: AddressZero, + }); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.eql( + 'shouldUnwrapNativeAsset and singleTokenMaxOut should not have conflicting values' + ); + }); + }); + }); +}); diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts new file mode 100644 index 000000000..e9cf66737 --- /dev/null +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/exit.concern.ts @@ -0,0 +1,221 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; +import * as SOR from '@balancer-labs/sor'; +import { + ExitConcern, + ExitExactBPTInSingleTokenOutParameters, + ExitExactTokensOutParameters, + ExitPool, + ExitPoolAttributes, +} from '../types'; +import { AssetHelpers, parsePoolInfo } from '@/lib/utils'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { addSlippage, subSlippage } from '@/lib/utils/slippageHelper'; +import { balancerVault } from '@/lib/constants/config'; +import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; +import { ComposableStablePoolEncoder } from '@/pool-composable-stable'; + +export class ComposableStablePoolExit implements ExitConcern { + buildExitSingleTokenOut = ({ + exiter, + pool, + bptIn, + slippage, + shouldUnwrapNativeAsset, + wrappedNativeAsset, + singleTokenMaxOut, + }: ExitExactBPTInSingleTokenOutParameters): ExitPoolAttributes => { + if (!bptIn.length || parseFixed(bptIn, 18).isNegative()) { + throw new BalancerError(BalancerErrorCode.INPUT_OUT_OF_BOUNDS); + } + if ( + singleTokenMaxOut && + singleTokenMaxOut !== AddressZero && + !pool.tokens.map((t) => t.address).some((a) => a === singleTokenMaxOut) + ) { + throw new BalancerError(BalancerErrorCode.TOKEN_MISMATCH); + } + + if (!shouldUnwrapNativeAsset && singleTokenMaxOut === AddressZero) + throw new Error( + 'shouldUnwrapNativeAsset and singleTokenMaxOut should not have conflicting values' + ); + + // Check if there's any relevant stable pool info missing + if (pool.tokens.some((token) => !token.decimals)) + throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); + if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); + + // Parse pool info into EVM amounts in order to match amountsIn scalling + const { + parsedTokens, + parsedBalances, + parsedAmp, + parsedTotalShares, + parsedSwapFee, + } = parsePoolInfo(pool); + + // Replace WETH address with ETH - required for exiting with ETH + const unwrappedTokens = parsedTokens.map((token) => + token === wrappedNativeAsset ? AddressZero : token + ); + + // Sort pool info based on tokens addresses + const assetHelpers = new AssetHelpers(wrappedNativeAsset); + const [sortedTokens, sortedBalances] = assetHelpers.sortTokens( + shouldUnwrapNativeAsset ? unwrappedTokens : parsedTokens, + parsedBalances + ) as [string[], string[]]; + + const minAmountsOut = Array(parsedTokens.length).fill('0'); + + // Exit pool with single token using exact bptIn + const singleTokenMaxOutIndex = parsedTokens.indexOf(singleTokenMaxOut); + + // Calculate amount out given BPT in + const amountOut = SOR.StableMathBigInt._calcTokenOutGivenExactBptIn( + BigInt(parsedAmp as string), + sortedBalances.map((b) => BigInt(b)), + singleTokenMaxOutIndex, + BigInt(bptIn), + BigInt(parsedTotalShares), + BigInt(parsedSwapFee) + ).toString(); + + // Apply slippage tolerance + minAmountsOut[singleTokenMaxOutIndex] = subSlippage( + BigNumber.from(amountOut), + BigNumber.from(slippage) + ).toString(); + + const userData = ComposableStablePoolEncoder.exitExactBPTInForOneTokenOut( + bptIn, + singleTokenMaxOutIndex + ); + + const to = balancerVault; + const functionName = 'exitPool'; + const attributes: ExitPool = { + poolId: pool.id, + sender: exiter, + recipient: exiter, + exitPoolRequest: { + assets: sortedTokens, + minAmountsOut, + userData, + toInternalBalance: false, + }, + }; + + // Encode transaction data into an ABI byte string which can be sent to the network to be executed + const vaultInterface = Vault__factory.createInterface(); + const data = vaultInterface.encodeFunctionData(functionName, [ + attributes.poolId, + attributes.sender, + attributes.recipient, + attributes.exitPoolRequest, + ]); + + return { + to, + functionName, + attributes, + data, + minAmountsOut, + maxBPTIn: bptIn, + }; + }; + + buildExitExactTokensOut = ({ + exiter, + pool, + tokensOut, + amountsOut, + slippage, + wrappedNativeAsset, + }: ExitExactTokensOutParameters): ExitPoolAttributes => { + if ( + tokensOut.length != amountsOut.length || + tokensOut.length != pool.tokensList.length + ) { + throw new BalancerError(BalancerErrorCode.INPUT_LENGTH_MISMATCH); + } + + // Check if there's any relevant stable pool info missing + if (pool.tokens.some((token) => !token.decimals)) + throw new BalancerError(BalancerErrorCode.MISSING_DECIMALS); + if (!pool.amp) throw new BalancerError(BalancerErrorCode.MISSING_AMP); + + // Parse pool info into EVM amounts in order to match amountsOut scalling + const { + parsedTokens, + parsedBalances, + parsedAmp, + parsedTotalShares, + parsedSwapFee, + } = parsePoolInfo(pool); + + // Sort pool info based on tokens addresses + const assetHelpers = new AssetHelpers(wrappedNativeAsset); + const [, sortedBalances] = assetHelpers.sortTokens( + parsedTokens, + parsedBalances + ) as [string[], string[]]; + const [sortedTokens, sortedAmounts] = assetHelpers.sortTokens( + tokensOut, + amountsOut + ) as [string[], string[]]; + + // Calculate expected BPT in given tokens out + const bptIn = SOR.StableMathBigInt._calcBptInGivenExactTokensOut( + BigInt(parsedAmp as string), + sortedBalances.map((b) => BigInt(b)), + sortedAmounts.map((a) => BigInt(a)), + BigInt(parsedTotalShares), + BigInt(parsedSwapFee) + ).toString(); + + // Apply slippage tolerance + const maxBPTIn = addSlippage( + BigNumber.from(bptIn), + BigNumber.from(slippage) + ).toString(); + + const userData = ComposableStablePoolEncoder.exitBPTInForExactTokensOut( + sortedAmounts, + maxBPTIn + ); + + const to = balancerVault; + const functionName = 'exitPool'; + const attributes: ExitPool = { + poolId: pool.id, + sender: exiter, + recipient: exiter, + exitPoolRequest: { + assets: sortedTokens, + minAmountsOut: sortedAmounts, + userData, + toInternalBalance: false, + }, + }; + + // encode transaction data into an ABI byte string which can be sent to the network to be executed + const vaultInterface = Vault__factory.createInterface(); + const data = vaultInterface.encodeFunctionData(functionName, [ + attributes.poolId, + attributes.sender, + attributes.recipient, + attributes.exitPoolRequest, + ]); + + return { + to, + functionName, + attributes, + data, + minAmountsOut: sortedAmounts, + maxBPTIn, + }; + }; +} diff --git a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/exit.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/exit.concern.integration.spec.ts index ffd4beb9d..4cc9bdb27 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/exit.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/exit.concern.integration.spec.ts @@ -41,7 +41,7 @@ const pool = pools_14717479.find( const tokensOut = pool.tokens; const controller = Pools.wrap(pool, networkConfig); -describe('exit execution', async () => { +describe('exit meta stable pools execution', async () => { let amountsOut: string[]; let transactionReceipt: TransactionReceipt; let bptBalanceBefore: BigNumber; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.integration.spec.ts index d202582ce..a13d3f2ff 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/exit.concern.integration.spec.ts @@ -40,7 +40,7 @@ const pool = pools_14717479.find( const tokensOut = pool.tokens; const controller = Pools.wrap(pool, networkConfig); -describe('exit execution', async () => { +describe('exit stable pools execution', async () => { let amountsOut: string[]; let transactionReceipt: TransactionReceipt; let bptBalanceBefore: BigNumber; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/types.ts b/balancer-js/src/modules/pools/pool-types/concerns/types.ts index 1878c0eab..d7f02e6b3 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/types.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/types.ts @@ -42,7 +42,7 @@ export interface ExitConcern { * @param singleTokenMaxOut Optional: token address that if provided will exit to given token * @returns transaction request ready to send with signer.sendTransaction */ - buildExitExactBPTIn: ({ + buildExitExactBPTIn?: ({ exiter, pool, bptIn, @@ -70,6 +70,16 @@ export interface ExitConcern { slippage, wrappedNativeAsset, }: ExitExactTokensOutParameters) => ExitPoolAttributes; + + buildExitSingleTokenOut?: ({ + exiter, + pool, + bptIn, + slippage, + shouldUnwrapNativeAsset, + wrappedNativeAsset, + singleTokenMaxOut, + }: ExitExactBPTInSingleTokenOutParameters) => ExitPoolAttributes; } export interface JoinPool { @@ -123,6 +133,16 @@ export interface ExitExactBPTInParameters { singleTokenMaxOut?: string; } +export interface ExitExactBPTInSingleTokenOutParameters { + exiter: string; + pool: Pool; + bptIn: string; + slippage: string; + shouldUnwrapNativeAsset: boolean; + wrappedNativeAsset: string; + singleTokenMaxOut: string; +} + export interface ExitExactTokensOutParameters { exiter: string; pool: Pool; diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.integration.spec.ts index 56ed1d6a5..111061ed1 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/exit.concern.integration.spec.ts @@ -53,7 +53,7 @@ let tokensMinBalanceIncrease: BigNumber[]; let transactionCost: BigNumber; let signerAddress: string; -describe('exit execution', async () => { +describe('exit weighted pools execution', async () => { // Setup chain before(async function () { this.timeout(20000); diff --git a/balancer-js/src/modules/relayer/relayer.module.ts b/balancer-js/src/modules/relayer/relayer.module.ts index e5086fff2..43ea48dfb 100644 --- a/balancer-js/src/modules/relayer/relayer.module.ts +++ b/balancer-js/src/modules/relayer/relayer.module.ts @@ -21,9 +21,11 @@ import { } from '../swaps/types'; import { SubgraphPoolBase } from '@balancer-labs/sor'; -import relayerLibraryAbi from '@/lib/abi/VaultActions.json'; +import relayerLibraryAbi from '@/lib/abi/BatchRelayerLibrary.json'; import aaveWrappingAbi from '@/lib/abi/AaveWrapping.json'; +const relayerLibrary = new Interface(relayerLibraryAbi); + export * from './types'; export class Relayer { @@ -39,9 +41,54 @@ export class Relayer { } } - static encodeBatchSwap(params: EncodeBatchSwapInput): string { - const relayerLibrary = new Interface(relayerLibraryAbi); + static encodeApproveVault(tokenAddress: string, maxAmount: string): string { + return relayerLibrary.encodeFunctionData('approveVault', [ + tokenAddress, + maxAmount, + ]); + } + + static encodeSetRelayerApproval( + relayerAdress: string, + approved: boolean, + authorisation: string + ): string { + return relayerLibrary.encodeFunctionData('setRelayerApproval', [ + relayerAdress, + approved, + authorisation, + ]); + } + + static encodeGaugeWithdraw( + gaugeAddress: string, + sender: string, + recipient: string, + amount: string + ): string { + return relayerLibrary.encodeFunctionData('gaugeWithdraw', [ + gaugeAddress, + sender, + recipient, + amount, + ]); + } + static encodeGaugeDeposit( + gaugeAddress: string, + sender: string, + recipient: string, + amount: string + ): string { + return relayerLibrary.encodeFunctionData('gaugeDeposit', [ + gaugeAddress, + sender, + recipient, + amount, + ]); + } + + static encodeBatchSwap(params: EncodeBatchSwapInput): string { return relayerLibrary.encodeFunctionData('batchSwap', [ params.swapType, params.swaps, @@ -55,8 +102,6 @@ export class Relayer { } static encodeExitPool(params: EncodeExitPoolInput): string { - const relayerLibrary = new Interface(relayerLibraryAbi); - return relayerLibrary.encodeFunctionData('exitPool', [ params.poolId, params.poolKind, diff --git a/balancer-js/src/modules/relayer/types.ts b/balancer-js/src/modules/relayer/types.ts index 9bf072316..dae4ac4f7 100644 --- a/balancer-js/src/modules/relayer/types.ts +++ b/balancer-js/src/modules/relayer/types.ts @@ -1,6 +1,6 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; -import { ExitPoolRequest } from '@/types'; +import { ExitPoolRequest, JoinPoolRequest } from '@/types'; import { SwapType, BatchSwapStep, @@ -33,6 +33,15 @@ export interface EncodeExitPoolInput { exitPoolRequest: ExitPoolRequest; } +export interface EncodeJoinPoolInput { + poolId: string; + poolKind: number; + sender: string; + recipient: string; + outputReferences: OutputReference[]; + joinPoolRequest: JoinPoolRequest; +} + export interface EncodeUnwrapAaveStaticTokenInput { staticToken: string; sender: string; diff --git a/balancer-js/src/modules/sdk.module.ts b/balancer-js/src/modules/sdk.module.ts index c5a11b003..07288810c 100644 --- a/balancer-js/src/modules/sdk.module.ts +++ b/balancer-js/src/modules/sdk.module.ts @@ -6,6 +6,7 @@ import { Sor } from './sor/sor.module'; import { getNetworkConfig } from './sdk.helpers'; import { Pricing } from './pricing/pricing.module'; import { ContractInstances, Contracts } from './contracts/contracts.module'; +import { Zaps } from './zaps/zaps.module'; import { Pools } from './pools'; import { Data } from './data'; import { Provider } from '@ethersproject/providers'; @@ -29,6 +30,7 @@ export class BalancerSDK implements BalancerSDKRoot { readonly pools: Pools; readonly data: Data; balancerContracts: Contracts; + zaps: Zaps; readonly networkConfig: BalancerNetworkConfig; constructor( @@ -48,6 +50,7 @@ export class BalancerSDK implements BalancerSDKRoot { this.networkConfig.addresses.contracts, sor.provider ); + this.zaps = new Zaps(this.networkConfig.chainId); } get rpcProvider(): Provider { diff --git a/balancer-js/src/modules/swaps/swap_builder/swap_utils.ts b/balancer-js/src/modules/swaps/swap_builder/swap_utils.ts index a244d785e..21c9880e4 100644 --- a/balancer-js/src/modules/swaps/swap_builder/swap_utils.ts +++ b/balancer-js/src/modules/swaps/swap_builder/swap_utils.ts @@ -1,5 +1,5 @@ import { Vault__factory } from '@balancer-labs/typechain'; -import vaultActionsAbi from '@/lib/abi/VaultActions.json'; +import BatchRelayerLibraryAbi from '@/lib/abi/BatchRelayerLibrary.json'; import { JsonFragment } from '@ethersproject/abi'; import { networkAddresses } from '@/lib/constants/config'; @@ -86,7 +86,7 @@ function relayerResolver( function swapFragment(relayer: SwapRelayer): JsonFragment[] { let source = Vault__factory.abi; - if (relayer.id === Relayers.lido) source = vaultActionsAbi; + if (relayer.id === Relayers.lido) source = BatchRelayerLibraryAbi; const signatures = source.filter( (fn) => fn.name && ['swap', 'batchSwap'].includes(fn.name) @@ -103,7 +103,7 @@ function batchSwapFragment( const vaultSignaturesForSwaps = Vault__factory.abi.filter( (fn) => fn.name && ['batchSwap'].includes(fn.name) ); - const relayerSignaturesForSwaps = vaultActionsAbi.filter( + const relayerSignaturesForSwaps = BatchRelayerLibraryAbi.filter( (fn) => fn.name && ['batchSwap'].includes(fn.name) ); let returnSignatures = vaultSignaturesForSwaps; diff --git a/balancer-js/src/modules/swaps/swaps.module.spec.ts b/balancer-js/src/modules/swaps/swaps.module.spec.ts index 0c80743c4..68d850b5c 100644 --- a/balancer-js/src/modules/swaps/swaps.module.spec.ts +++ b/balancer-js/src/modules/swaps/swaps.module.spec.ts @@ -12,7 +12,7 @@ import { getNetworkConfig } from '@/modules/sdk.helpers'; import { mockPool, mockPoolDataService } from '@/test/lib/mockPool'; import { SwapTransactionRequest, SwapType } from './types'; import { Vault__factory } from '@balancer-labs/typechain'; -import vaultActionsAbi from '@/lib/abi/VaultActions.json'; +import BatchRelayerLibraryAbi from '@/lib/abi/BatchRelayerLibrary.json'; import { Interface } from '@ethersproject/abi'; import { BigNumber } from '@ethersproject/bignumber'; import { AddressZero, MaxUint256 } from '@ethersproject/constants'; @@ -34,7 +34,7 @@ const sdkConfig: BalancerSdkConfig = { }; const vaultInterface = Vault__factory.createInterface(); -const vaultActions = new Interface(vaultActionsAbi); +const vaultActions = new Interface(BatchRelayerLibraryAbi); const funds = { fromInternalBalance: false, diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/addresses.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/addresses.ts new file mode 100644 index 000000000..d911d7f50 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/addresses.ts @@ -0,0 +1,203 @@ +export const ADDRESSES = { + 1: { + relayer: '0x886A3Ec7bcC508B8795990B60Fa21f85F9dB7948', + staBal3: { + id: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063', + address: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42', + gauge: '0x34f33cdaed8ba0e1ceece80e5f4a73bcf234cfac', + assetOrder: ['DAI', 'USDC', 'USDT'], + }, + bbausd1: { + id: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe', + address: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2', + gauge: '0x68d019f64a7aa97e2d4e7363aee42251d08124fb', + assetOrder: ['bb-a-USDT', 'bb-a-DAI', 'bb-a-USDC'], + }, + bbausd2: { + id: '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d', + address: '0xa13a9247ea42d743238089903570127dda72fe44', + gauge: '0xa6325e799d266632d347e41265a69af111b05403', + }, + linearUsdc1: { + id: '0x9210f1204b5a24742eba12f710636d76240df3d00000000000000000000000fc', + address: '0x9210F1204b5a24742Eba12f710636D76240dF3d0', + }, + linearDai1: { + id: '0x804cdb9116a10bb78768d3252355a1b18067bf8f0000000000000000000000fb', + address: '0x804CdB9116a10bB78768D3252355a1b18067bF8f', + }, + linearUsdt1: { + id: '0x2bbf681cc4eb09218bee85ea2a5d3d13fa40fc0c0000000000000000000000fd', + address: '0x2BBf681cC4eb09218BEe85EA2a5d3D13Fa40fC0C', + }, + linearUsdc2: { + id: '0x82698AECC9E28E9BB27608BD52CF57F704BD1B83000000000000000000000336', + address: '0x82698aeCc9E28e9Bb27608Bd52cF57f704BD1B83', + }, + linearDai2: { + id: '0xAE37D54AE477268B9997D4161B96B8200755935C000000000000000000000337', + address: '0xae37D54Ae477268B9997d4161B96b8200755935c', + }, + linearUsdt2: { + id: '0x2F4EB100552EF93840D5ADC30560E5513DFFFACB000000000000000000000334', + address: '0x2F4eb100552ef93840d5aDC30560E5513DFfFACb', + }, + maiusd: { + id: '', + address: '', + gauge: '', + assetOrder: ['USDT', 'miMATIC', 'DAI', 'USDC'], + }, + maibbausd: { + id: '', + address: '', + gauge: '', + assetOrder: ['bb-a-USD', 'miMATIC'], + }, + DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', + USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + waDAI: '0x02d60b84491589974263d922d9cc7a3152618ef6', + waUSDC: '0xd093fa4fb80d09bb30817fdcd442d4d02ed3e5de', + waUSDT: '0xf8fd466f12e236f4c96f7cce6c79eadb819abf58', + miMATIC: '', + }, + 5: { + relayer: '0x7b9B6f094DC2Bd1c12024b0D9CC63d6993Be1888', + staBal3: { + id: '0xdcdd4a3d36dec8d57594e89763d069a7e9b223e2000000000000000000000062', + address: '0xdcdd4a3d36dec8d57594e89763d069a7e9b223e2', + gauge: '0xfd364cda96bb7db06b65706182c448a73f0a5f9a', + assetOrder: ['USDT', 'DAI', 'USDC'], + }, + staBal3_2: { + id: '0xff9d677474d4344379924e10b68c8fea67e03294000000000000000000000072', + address: '0xff9d677474d4344379924e10b68c8fea67e03294', + gauge: '0x4e4ebf2aa90e41174d716a5168895357762d68af', + assetOrder: ['USDT', 'DAI', 'USDC'], + }, + staBal3_3: { + id: '0x3bfc8a0509f1a68aefd446f6c19bf37b3c75a8fc0000000000000000000000a5', + address: '0x3bfc8a0509f1a68aefd446f6c19bf37b3c75a8fc', + gauge: '0x7776e1008d7c20ab54aa57a7c44fc7de602de29a', + assetOrder: ['USDT', 'DAI', 'USDC'], + }, + bbausd1: { + id: '0x13acd41c585d7ebb4a9460f7c8f50be60dc080cd00000000000000000000005f', + address: '0x13acd41c585d7ebb4a9460f7c8f50be60dc080cd', + gauge: '0xa2d0ea81a47d68598922cd54c59249ff58c2a3ff', + assetOrder: ['bb-a-USDC', 'bb-a-DAI', 'bb-a-USDT'], + }, + bbausd2: { + id: '0x13acd41c585d7ebb4a9460f7c8f50be60dc080cd00000000000000000000005f', + address: '0x13acd41c585d7ebb4a9460f7c8f50be60dc080cd', + gauge: '0xa2d0ea81a47d68598922cd54c59249ff58c2a3ff', + }, + linearUsdc1: { + id: '0x0595d1df64279ddb51f1bdc405fe2d0b4cc8668100000000000000000000005c', + address: '0x0595d1df64279ddb51f1bdc405fe2d0b4cc86681', + }, + linearDai1: { + id: '0x5cea6a84ed13590ed14903925fa1a73c36297d9900000000000000000000005d', + address: '0x5cea6a84ed13590ed14903925fa1a73c36297d99', + }, + linearUsdt1: { + id: '0xefd681a82970ac5d980b9b2d40499735e7bf3f1f00000000000000000000005e', + address: '0xefd681a82970ac5d980b9b2d40499735e7bf3f1f', + }, + linearUsdc2: { + id: '0x0595d1df64279ddb51f1bdc405fe2d0b4cc8668100000000000000000000005c', + address: '0x0595d1df64279ddb51f1bdc405fe2d0b4cc86681', + }, + linearDai2: { + id: '0x5cea6a84ed13590ed14903925fa1a73c36297d9900000000000000000000005d', + address: '0x5cea6a84ed13590ed14903925fa1a73c36297d99', + }, + linearUsdt2: { + id: '0xefd681a82970ac5d980b9b2d40499735e7bf3f1f00000000000000000000005e', + address: '0xefd681a82970ac5d980b9b2d40499735e7bf3f1f', + }, + maiusd: { + id: '0x6a8f9ab364b85725973d2a33cb9aae2dac43b5e30000000000000000000000a6', + address: '0x6a8f9ab364b85725973d2a33cb9aae2dac43b5e3', + gauge: '0x58141bdcecb7fbae006964f4131cf6f65c948357', + assetOrder: ['USDT', 'miMATIC', 'DAI', 'USDC'], + }, + maibbausd: { + id: '0xb04b03b78cf79788a1931545bd2744161029648f0000000000000000000000a8', + address: '0xb04b03b78cf79788a1931545bd2744161029648f', + gauge: '0xdc3f6fc8898830e53c777543fe252b14f22680d4', + assetOrder: ['bb-a-USD', 'miMATIC', 'MAI BSP'], + }, + USDT: '0x1f1f156e0317167c11aa412e3d1435ea29dc3cce', + DAI: '0x8c9e6c40d3402480ace624730524facc5482798c', + USDC: '0xe0c9275e44ea80ef17579d33c55136b7da269aeb', + waDAI: '0x89534a24450081aa267c79b07411e9617d984052', + waUSDC: '0x811151066392fd641fe74a9b55a712670572d161', + waUSDT: '0x4cb1892fddf14f772b2e39e299f44b2e5da90d04', + miMATIC: '0x398106564948feeb1fedea0709ae7d969d62a391', + }, + 137: { + relayer: '0xcf6a66E32dCa0e26AcC3426b851FD8aCbF12Dac7', + staBal3: { + id: '', + address: '', + gauge: '', + assetOrder: ['USDT', 'DAI', 'USDC'], + }, + bbausd1: { + id: '', + address: '', + gauge: '', + assetOrder: ['bb-a-USDC', 'bb-a-DAI', 'bb-a-USDT'], + }, + bbausd2: { + id: '0x48e6b98ef6329f8f0a30ebb8c7c960330d64808500000000000000000000075b', + address: '0x48e6b98ef6329f8f0a30ebb8c7c960330d648085', + gauge: '', + }, + linearUsdc1: { + id: '', + address: '', + }, + linearDai1: { + id: '', + address: '', + }, + linearUsdt1: { + id: '', + address: '', + }, + linearUsdc2: { + id: '0xf93579002dbe8046c43fefe86ec78b1112247bb8000000000000000000000759', + address: '0xf93579002dbe8046c43fefe86ec78b1112247bb8', + }, + linearDai2: { + id: '0x178e029173417b1f9c8bc16dcec6f697bc323746000000000000000000000758', + address: '0x178e029173417b1f9c8bc16dcec6f697bc323746', + }, + linearUsdt2: { + id: '0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea600000000000000000000075a', + address: '0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6', + }, + maiusd: { + id: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000012', + address: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42', + gauge: '0x72843281394e68de5d55bcf7072bb9b2ebc24150', + assetOrder: ['USDC', 'DAI', 'miMATIC', 'USDT'], + }, + maibbausd: { + id: '0xb54b2125b711cd183edd3dd09433439d5396165200000000000000000000075e', + address: '0xb54b2125b711cd183edd3dd09433439d53961652', + gauge: '0x9a105ef22a59484aa2731c357049f6a13d0891f5', + assetOrder: ['bb-a-USD', 'miMATIC'], + }, + USDT: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + DAI: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + waDAI: '0xEE029120c72b0607344f35B17cdD90025e647B00', + waUSDC: '0x221836a597948Dce8F3568E044fF123108aCc42A', + waUSDT: '0x19C60a251e525fa88Cd6f3768416a8024e98fC19', + miMATIC: '0xa3fa99a148fa48d14ed51d610c367c61876997f1', + }, +}; diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.integration.spec.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.integration.spec.ts new file mode 100644 index 000000000..c602b35da --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.integration.spec.ts @@ -0,0 +1,272 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import hardhat from 'hardhat'; +import { + BalancerError, + BalancerErrorCode, + BalancerSDK, + Network, + RelayerAuthorization, + PoolWithMethods, +} from '@/.'; +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { Contracts } from '@/modules/contracts/contracts.module'; +import { ADDRESSES } from './addresses'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { MaxUint256, WeiPerEther } from '@ethersproject/constants'; +import { Migrations } from '../migrations'; +import { getErc20Balance, move, stake } from '@/test/lib/utils'; + +dotenv.config(); +const { ALCHEMY_URL: jsonRpcUrl } = process.env; + +/* + * Testing on GOERLI + * - Update hardhat.config.js with chainId = 5 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +// const network = Network.GOERLI; +// const blockNumber = 7277540; +// const holderAddress = '0xd86a11b0c859c18bfc1b4acd072c5afe57e79438'; + +/* + * Testing on MAINNET + * - Update hardhat.config.js with chainId = 1 + * - Update ALCHEMY_URL on .env with a mainnet api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +const network = Network.MAINNET; +const blockNumber = 15496800; +const holderAddress = '0xec576a26335de1c360d2fc9a68cba6ba37af4a13'; + +const { ethers } = hardhat; +const MAX_GAS_LIMIT = 8e6; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); +const addresses = ADDRESSES[network]; +const fromPool = { + id: addresses.bbausd1.id, + address: addresses.bbausd1.address, + gauge: addresses.bbausd1.gauge, +}; +const toPool = { + id: addresses.bbausd2.id, + address: addresses.bbausd2.address, + gauge: addresses.bbausd2.gauge, +}; +const { contracts } = new Contracts(network as number, provider); +const migrations = new Migrations(network); + +const relayer = addresses.relayer; + +const signRelayerApproval = async ( + relayerAddress: string, + signerAddress: string, + signer: JsonRpcSigner +): Promise => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayerAddress, true] + ); + + const signature = + await RelayerAuthorization.signSetRelayerApprovalAuthorization( + contracts.vault, + signer, + relayerAddress, + approval + ); + + const calldata = RelayerAuthorization.encodeCalldataAuthorization( + '0x', + MaxUint256, + signature + ); + + return calldata; +}; + +const reset = async () => + provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber, + }, + }, + ]); + +describe('bbausd migration execution', async () => { + let signer: JsonRpcSigner; + let signerAddress: string; + let authorisation: string; + let balance: BigNumber; + let pool: PoolWithMethods; + + beforeEach(async function () { + await reset(); + + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + authorisation = await signRelayerApproval(relayer, signerAddress, signer); + // Transfer tokens from existing user account to signer + // We need that to test signatures, because hardhat doesn't have impersonated accounts private keys + balance = await move( + fromPool.address, + holderAddress, + signerAddress, + provider + ); + + const sdk = new BalancerSDK({ + network, + rpcUrl, + }); + const { pools } = sdk; + await pools.findBy('address', fromPool.address).then((res) => { + if (!res) throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST); + pool = res; + }); + }); + + async function testFlow( + staked: boolean, + authorised = true, + minOutBuffer: string + ): Promise { + const addressIn = staked ? fromPool.gauge : fromPool.address; + const addressOut = staked ? toPool.gauge : toPool.address; + // Store balance before migration + const before = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + const amount = before.from; + + let query = migrations.bbaUsd( + signerAddress, + amount.toString(), + '0', + staked, + pool.tokens + .filter((token) => token.symbol !== 'bb-a-USD') // Note that bbausd is removed + .map((token) => { + const parsedBalance = parseFixed(token.balance, token.decimals); + const parsedPriceRate = parseFixed(token.priceRate as string, 18); + return parsedBalance.mul(WeiPerEther).div(parsedPriceRate).toString(); + }), + authorisation + ); + + const gasLimit = MAX_GAS_LIMIT; + + // Static call can be used to simulate tx and get expected BPT in/out deltas + const staticResult = await signer.call({ + to: query.to, + data: query.data, + gasLimit, + }); + + const bptOut = query.decode(staticResult, staked); + + query = migrations.bbaUsd( + signerAddress, + amount.toString(), + BigNumber.from(bptOut).add(minOutBuffer).toString(), + staked, + pool.tokens + .filter((token) => token.symbol !== 'bb-a-USD') // Note that bbausd is removed + .map((token) => { + const parsedBalance = parseFixed(token.balance, token.decimals); + const parsedPriceRate = parseFixed(token.priceRate as string, 18); + return parsedBalance.mul(WeiPerEther).div(parsedPriceRate).toString(); + }), + authorised ? authorisation : undefined + ); + + const response = await signer.sendTransaction({ + to: query.to, + data: query.data, + gasLimit, + }); + + const receipt = await response.wait(); + console.log('Gas used', receipt.gasUsed.toString()); + + const after = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + expect(BigNumber.from(bptOut).gt(0)).to.be.true; + expect(after.from.toString()).to.eq('0'); + expect(after.to.gte(bptOut)).to.be.true; + return bptOut; + } + + context('staked', async () => { + beforeEach(async function () { + // Stake them + await stake(signer, fromPool.address, fromPool.gauge, balance); + }); + + it('should transfer tokens from stable to boosted - using exact bbausd2AmountOut from static call', async () => { + await testFlow(true, undefined, '0'); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(true, true, '1000000000000000000'); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('not staked', async () => { + it('should transfer tokens from stable to boosted - using exact bbausd2AmountOut from static call', async () => { + await testFlow(false, undefined, '0'); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, true, '1000000000000000000'); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('authorisation', async () => { + // authorisation wihtin relayer is the default case and is already tested on previous scenarios + + it('should transfer tokens from stable to boosted - pre authorised', async () => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayer, true] + ); + await signer.sendTransaction({ + to: contracts.vault.address, + data: approval, + }); + await testFlow(false, false, '0'); + }); + + it('should transfer tokens from stable to boosted - auhtorisation should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, false, '0'); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#503'); // USER_DOESNT_ALLOW_RELAYER - Relayers must be allowed by both governance and the user account + }); + }); +}); diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.ts new file mode 100644 index 000000000..a198d53b9 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/bbausd1.ts @@ -0,0 +1,316 @@ +import { ADDRESSES } from './addresses'; +import { Relayer } from '@/modules/relayer/relayer.module'; +import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types'; +import { Interface } from '@ethersproject/abi'; +// TODO - Ask Nico to update Typechain? +import balancerRelayerAbi from '@/lib/abi/BalancerRelayer.json'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Zero } from '@ethersproject/constants'; +import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; +const balancerRelayerInterface = new Interface(balancerRelayerAbi); + +const SWAP_RESULT_BBAUSD = Relayer.toChainedReference('24'); +export class BbaUsd1Builder { + private addresses; + + constructor(networkId: 1 | 5 | 137) { + this.addresses = ADDRESSES[networkId]; + } + + /** + * Builds migration call data. + * Migrates tokens from bbausd1 to bbausd2 pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param bbausd1Amount Amount of BPT tokens to migrate. + * @param minBbausd2Out Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param tokenBalances Token balances in EVM scale. Array must have the same length and order as tokens in pool being migrated from. Refer to [getPoolTokens](https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/vault/IVault.sol#L334). + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + calldata( + userAddress: string, + bbausd1Amount: string, + minBbausd2Out: string, + staked: boolean, + tokenBalances: string[], + authorisation?: string + ): { + to: string; + data: string; + } { + if (BigNumber.from(bbausd1Amount).lte(0)) + throw new BalancerError(BalancerErrorCode.INPUT_ZERO_NOT_ALLOWED); + const relayer = this.addresses.relayer; + let calls: string[] = []; + + if (authorisation) { + calls = [this.buildSetRelayerApproval(authorisation)]; + } + + if (staked) { + calls = [ + ...calls, + this.buildWithdraw(userAddress, bbausd1Amount), + this.buildSwap( + bbausd1Amount, + minBbausd2Out, + relayer, + relayer, + tokenBalances + ), + this.buildDeposit(userAddress), + ]; + } else { + calls = [ + ...calls, + this.buildSwap( + bbausd1Amount, + minBbausd2Out, + userAddress, + userAddress, + tokenBalances + ), + ]; + } + + const callData = balancerRelayerInterface.encodeFunctionData('multicall', [ + calls, + ]); + + return { + to: this.addresses.relayer, + data: callData, + }; + } + + /** + * Creates encoded batchSwap function with following swaps: boosted -> linears -> stables -> linears -> boosted + * outputreferences should contain the amount of resulting BPT. + * + * @param bbausd1Amount Amount of BPT tokens to migrate. + * @param minBbausd2Out Minimum of expected BPT out ot the migration flow. + * @param sender Sender address. + * @param recipient Recipient address. + * @param tokenBalances Token balances in EVM scale. + * @returns Encoded batchSwap call. Output references. + */ + buildSwap( + bbausd1Amount: string, + minBbausd2Out: string, + sender: string, + recipient: string, + tokenBalances: string[] + ): string { + const assets = [ + this.addresses.bbausd2.address, + this.addresses.waDAI, + this.addresses.linearDai1.address, + this.addresses.linearDai2.address, + this.addresses.waUSDC, + this.addresses.linearUsdc1.address, + this.addresses.linearUsdc2.address, + this.addresses.waUSDT, + this.addresses.linearUsdt1.address, + this.addresses.linearUsdt2.address, + this.addresses.bbausd1.address, + ]; + + const outputReferences = [{ index: 0, key: SWAP_RESULT_BBAUSD }]; + + // Calculate proportional token amounts + + // Assuming 1:1 exchange rates between tokens + // TODO: Fetch current prices, or use price or priceRate from subgraph? + const totalLiquidity = tokenBalances.reduce( + (sum, tokenBalance) => sum.add(BigNumber.from(tokenBalance)), + Zero + ); + + // bbausd1[bbausd1]blinear1[linear1]stable[linear2]blinear2[bbausd2]bbausd2 and then do that proportionally for each underlying stable. + // Split BPT amount proportionally: + const { assetOrder } = this.addresses.bbausd1; + const usdcBptAmt = BigNumber.from(bbausd1Amount) + .mul(tokenBalances[assetOrder.indexOf('bb-a-USDC')]) + .div(totalLiquidity) + .toString(); + const daiBptAmt = BigNumber.from(bbausd1Amount) + .mul(tokenBalances[assetOrder.indexOf('bb-a-DAI')]) + .div(totalLiquidity) + .toString(); + const usdtBptAmt = BigNumber.from(bbausd1Amount) + .sub(usdcBptAmt) + .sub(daiBptAmt) + .toString(); + + const swaps: BatchSwapStep[] = [ + { + poolId: this.addresses.bbausd1.id, + assetInIndex: 10, + assetOutIndex: 2, + amount: daiBptAmt, + userData: '0x', + }, + { + poolId: this.addresses.linearDai1.id, + assetInIndex: 2, + assetOutIndex: 1, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearDai2.id, + assetInIndex: 1, + assetOutIndex: 3, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 3, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.bbausd1.id, + assetInIndex: 10, + assetOutIndex: 5, + amount: usdcBptAmt, + userData: '0x', + }, + { + poolId: this.addresses.linearUsdc1.id, + assetInIndex: 5, + assetOutIndex: 4, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdc2.id, + assetInIndex: 4, + assetOutIndex: 6, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 6, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.bbausd1.id, + assetInIndex: 10, + assetOutIndex: 8, + amount: usdtBptAmt, + userData: '0x', + }, + { + poolId: this.addresses.linearUsdt1.id, + assetInIndex: 8, + assetOutIndex: 7, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdt2.id, + assetInIndex: 7, + assetOutIndex: 9, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 9, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + ]; + + // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number. + const limits = [ + BigNumber.from(minBbausd2Out).mul(-1).toString(), // bbausd2 + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + bbausd1Amount, // Max in should be bbausd1 amount + ]; + + // Swap to/from Relayer + const funds: FundManagement = { + sender, + recipient, + fromInternalBalance: false, + toInternalBalance: false, + }; + + const encodedBatchSwap = Relayer.encodeBatchSwap({ + swapType: SwapType.SwapExactIn, + swaps, + assets, + funds, + limits, + deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now + value: '0', + outputReferences, + }); + + return encodedBatchSwap; + } + + /** + * Uses relayer to withdraw staked BPT from gauge and send to relayer + * + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @returns withdraw call + */ + buildWithdraw(sender: string, amount: string): string { + return Relayer.encodeGaugeWithdraw( + this.addresses.bbausd1.gauge, + sender, + this.addresses.relayer, + amount + ); + } + + /** + * Uses relayer to deposit user's BPT to gauge and sends to recipient + * + * @param recipient Recipient address. + * @returns deposit call + */ + buildDeposit(recipient: string): string { + return Relayer.encodeGaugeDeposit( + this.addresses.bbausd2.gauge, + this.addresses.relayer, + recipient, + SWAP_RESULT_BBAUSD.toString() + ); + } + + /** + * Uses relayer to approve itself to act in behalf of the user + * + * @param authorisation Encoded authorisation call. + * @returns relayer approval call + */ + buildSetRelayerApproval(authorisation: string): string { + return Relayer.encodeSetRelayerApproval( + this.addresses.relayer, + true, + authorisation + ); + } +} diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.integration.spec.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.integration.spec.ts new file mode 100644 index 000000000..2da7b3cd2 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.integration.spec.ts @@ -0,0 +1,245 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import hardhat from 'hardhat'; +import { Network, RelayerAuthorization } from '@/.'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contracts } from '@/modules/contracts/contracts.module'; +import { ADDRESSES } from './addresses'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { MaxUint256 } from '@ethersproject/constants'; +import { Migrations } from '../migrations'; +import { getErc20Balance, move, stake } from '@/test/lib/utils'; + +/* + * Testing on GOERLI + * - Update hardhat.config.js with chainId = 5 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +const network = Network.GOERLI; +const blockNumber = 7376670; +const holderAddress = '0x8fe3a2a5ae6baa201c26fc7830eb713f33d6b313'; + +/* + * Testing on POLYGON + * - Update hardhat.config.js with chainId = 137 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +// const network = Network.POLYGON; +// const blockNumber = 32856000; +// const holderAddress = '0xfb0272990728a967ecaf702a1291fcd64c38ed25'; + +dotenv.config(); + +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const { ethers } = hardhat; +const MAX_GAS_LIMIT = 8e6; + +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); +const addresses = ADDRESSES[network]; +const fromPool = { + id: addresses.maiusd.id, + address: addresses.maiusd.address, + gauge: addresses.maiusd.gauge, +}; +const toPool = { + id: addresses.maibbausd.id, + address: addresses.maibbausd.address, + gauge: addresses.maibbausd.gauge, +}; +const { contracts } = new Contracts(network as number, provider); +const migrations = new Migrations(network); + +const relayer = addresses.relayer; + +const signRelayerApproval = async ( + relayerAddress: string, + signerAddress: string, + signer: JsonRpcSigner +): Promise => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayerAddress, true] + ); + + const signature = + await RelayerAuthorization.signSetRelayerApprovalAuthorization( + contracts.vault, + signer, + relayerAddress, + approval + ); + + const calldata = RelayerAuthorization.encodeCalldataAuthorization( + '0x', + MaxUint256, + signature + ); + + return calldata; +}; + +const reset = () => + provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber, + }, + }, + ]); + +describe('maiusd migration execution', async () => { + let signer: JsonRpcSigner; + let signerAddress: string; + let authorisation: string; + let balance: BigNumber; + + beforeEach(async function () { + await reset(); + + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + authorisation = await signRelayerApproval(relayer, signerAddress, signer); + + // Transfer tokens from existing user account to signer + // We need that to test signatures, because hardhat doesn't have impersonated accounts private keys + balance = await move( + fromPool.address, + holderAddress, + signerAddress, + provider + ); + }); + + async function testFlow( + staked: boolean, + authorised = true, + minBptOut: undefined | string = undefined + ): Promise { + const addressIn = staked ? fromPool.gauge : fromPool.address; + const addressOut = staked ? toPool.gauge : toPool.address; + // Store balance before migration + const before = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + const amount = before.from; + + let query = migrations.maiusd( + signerAddress, + amount.toString(), + '0', + staked, + authorisation + ); + const gasLimit = MAX_GAS_LIMIT; + + // Static call can be used to simulate tx and get expected BPT in/out deltas + const staticResult = await signer.call({ + to: query.to, + data: query.data, + gasLimit, + }); + const bptOut = query.decode(staticResult, staked); + + query = migrations.maiusd( + signerAddress, + amount.toString(), + minBptOut ? minBptOut : bptOut, + staked, + authorised ? authorisation : undefined + ); + + const response = await signer.sendTransaction({ + to: query.to, + data: query.data, + gasLimit, + }); + + const receipt = await response.wait(); + console.log('Gas used', receipt.gasUsed.toString()); + + const after = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + console.log(bptOut); + + expect(BigNumber.from(bptOut).gt(0)).to.be.true; + expect(after.from.toString()).to.eq('0'); + expect(after.to.toString()).to.eq(bptOut); + return bptOut; + } + + let bptOut: string; + + context('staked', async () => { + beforeEach(async function () { + // Stake them + await stake(signer, fromPool.address, fromPool.gauge, balance); + }); + + it('should transfer tokens from stable to boosted', async () => { + bptOut = await testFlow(true); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(true, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('not staked', async () => { + it('should transfer tokens from stable to boosted', async () => { + bptOut = await testFlow(false); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('authorisation', async () => { + // authorisation wihtin relayer is the default case and is already tested on previous scenarios + + it('should transfer tokens from stable to boosted - pre authorised', async () => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayer, true] + ); + await signer.sendTransaction({ + to: contracts.vault.address, + data: approval, + }); + await testFlow(false, false); + }); + + it('should transfer tokens from stable to boosted - auhtorisation should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, false); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#503'); // USER_DOESNT_ALLOW_RELAYER - Relayers must be allowed by both governance and the user account + }); + }); +}); diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.ts new file mode 100644 index 000000000..d6789ba39 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/maiusd.ts @@ -0,0 +1,304 @@ +import { StablePoolEncoder } from '@/pool-stable/encoder'; +import { ADDRESSES } from './addresses'; +import { Relayer } from '@/modules/relayer/relayer.module'; +import { ExitPoolRequest } from '@/types'; +import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types'; +import { Interface } from '@ethersproject/abi'; +import { BigNumber } from '@ethersproject/bignumber'; +import { MaxInt256 } from '@ethersproject/constants'; +import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; +// TODO - Ask Nico to update Typechain? +import balancerRelayerAbi from '@/lib/abi/BalancerRelayer.json'; +const balancerRelayerInterface = new Interface(balancerRelayerAbi); + +const EXIT_MIMATIC = Relayer.toChainedReference('20'); +const EXIT_DAI = Relayer.toChainedReference('21'); +const EXIT_USDC = Relayer.toChainedReference('22'); +const EXIT_USDT = Relayer.toChainedReference('23'); +const SWAP_RESULT = Relayer.toChainedReference('24'); + +export class MaiusdBuilder { + private addresses; + + constructor(networkId: 1 | 5 | 137) { + this.addresses = ADDRESSES[networkId]; + } + + /** + * Builds migration call data. + * Migrates tokens from maiusd to maibbausd pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param bptIn Amount of BPT tokens to migrate. + * @param minBptOut Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + calldata( + userAddress: string, + bptIn: string, + minBptOut: string, + staked: boolean, + authorisation?: string + ): { + to: string; + data: string; + } { + if (BigNumber.from(bptIn).lte(0)) + throw new BalancerError(BalancerErrorCode.INPUT_ZERO_NOT_ALLOWED); + + const relayer = this.addresses.relayer; + let calls: string[] = []; + + if (authorisation) { + calls = [this.buildSetRelayerApproval(authorisation)]; + } + + if (staked) { + calls = [ + ...calls, + this.buildWithdraw(userAddress, bptIn), + this.buildExit(relayer, bptIn), + this.buildSwap(relayer, minBptOut), + this.buildDeposit(userAddress), + ]; + } else { + calls = [ + ...calls, + this.buildExit(userAddress, bptIn), + this.buildSwap(userAddress, minBptOut), + ]; + } + + const callData = balancerRelayerInterface.encodeFunctionData('multicall', [ + calls, + ]); + + return { + to: relayer, + data: callData, + }; + } + + /** + * Encodes exitPool callData. + * Exit maiusd pool proportionally to underlying stables. Exits to relayer. + * Outputreferences are used to store exit amounts for next transaction. + * + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @returns Encoded exitPool call. Output references. + */ + buildExit(sender: string, amount: string): string { + const { assetOrder } = this.addresses.maiusd; + const assets = assetOrder.map( + (key) => this.addresses[key as keyof typeof this.addresses] as string + ); + + // Assume gaugeWithdraw returns same amount value + const userData = StablePoolEncoder.exitExactBPTInForTokensOut(amount); + + // Store exit outputs to be used as swaps inputs + const outputReferences = [ + { index: assetOrder.indexOf('miMATIC'), key: EXIT_MIMATIC }, + { index: assetOrder.indexOf('DAI'), key: EXIT_DAI }, + { index: assetOrder.indexOf('USDC'), key: EXIT_USDC }, + { index: assetOrder.indexOf('USDT'), key: EXIT_USDT }, + ]; + + const minAmountsOut = Array(assets.length).fill('0'); + + const callData = Relayer.constructExitCall({ + assets, + minAmountsOut, + userData, + toInternalBalance: true, + poolId: this.addresses.maiusd.id, + poolKind: 0, // This will always be 0 to match supported Relayer types + sender, + recipient: this.addresses.relayer, + outputReferences, + exitPoolRequest: {} as ExitPoolRequest, + }); + + return callData; + } + + /** + * Creates encoded batchSwap function with following swaps: stables -> linear pools -> boosted pool + * outputreferences should contain the amount of resulting BPT. + * + * @param recipient Sender address. + * @param minBptOut Minimum BPT out expected from the join transaction. + * @returns Encoded batchSwap call. Output references. + */ + buildSwap(recipient: string, minBptOut: string): string { + const assets = [ + this.addresses.bbausd2.address, + this.addresses.DAI, + this.addresses.linearDai2.address, + this.addresses.USDC, + this.addresses.linearUsdc2.address, + this.addresses.USDT, + this.addresses.linearUsdt2.address, + this.addresses.miMATIC, + this.addresses.maibbausd.address, + ]; + + const outputReferences = [{ index: 8, key: SWAP_RESULT }]; + + const swaps: BatchSwapStep[] = [ + { + poolId: this.addresses.linearDai2.id, + assetInIndex: 1, + assetOutIndex: 2, + amount: EXIT_DAI.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 2, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.maibbausd.id, + assetInIndex: 0, + assetOutIndex: 8, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdc2.id, + assetInIndex: 3, + assetOutIndex: 4, + amount: EXIT_USDC.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 4, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.maibbausd.id, + assetInIndex: 0, + assetOutIndex: 8, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdt2.id, + assetInIndex: 5, + assetOutIndex: 6, + amount: EXIT_USDT.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 6, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.maibbausd.id, + assetInIndex: 0, + assetOutIndex: 8, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.maibbausd.id, + assetInIndex: 7, + assetOutIndex: 8, + amount: EXIT_MIMATIC.toString(), + userData: '0x', + }, + ]; + + // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number. + const limits = [ + '0', + MaxInt256.toString(), + '0', + MaxInt256.toString(), + '0', + MaxInt256.toString(), + '0', + MaxInt256.toString(), + BigNumber.from(minBptOut).mul(-1).toString(), + ]; + + // Swap to/from Relayer + const funds: FundManagement = { + sender: this.addresses.relayer, + recipient, + fromInternalBalance: true, + toInternalBalance: false, + }; + + const encodedBatchSwap = Relayer.encodeBatchSwap({ + swapType: SwapType.SwapExactIn, + swaps, + assets, + funds, + limits, + deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now + value: '0', + outputReferences, + }); + + return encodedBatchSwap; + } + + /** + * Uses relayer to withdraw staked BPT from gauge and send to relayer + * + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @returns withdraw call + */ + buildWithdraw(sender: string, amount: string): string { + return Relayer.encodeGaugeWithdraw( + this.addresses.maiusd.gauge, + sender, + this.addresses.relayer, + amount + ); + } + + /** + * Uses relayer to deposit user's BPT to gauge and sends to recipient + * + * @param recipient Recipient address. + * @returns deposit call + */ + buildDeposit(recipient: string): string { + return Relayer.encodeGaugeDeposit( + this.addresses.maibbausd.gauge, + this.addresses.relayer, + recipient, + SWAP_RESULT.toString() + ); + } + + /** + * Uses relayer to approve itself to act in behalf of the user + * + * @param authorisation Encoded authorisation call. + * @returns relayer approval call + */ + buildSetRelayerApproval(authorisation: string): string { + return Relayer.encodeSetRelayerApproval( + this.addresses.relayer, + true, + authorisation + ); + } +} diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.integration.spec.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.integration.spec.ts new file mode 100644 index 000000000..6b73b1e92 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.integration.spec.ts @@ -0,0 +1,249 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import hardhat from 'hardhat'; +import { Network, RelayerAuthorization } from '@/.'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contracts } from '@/modules/contracts/contracts.module'; +import { ADDRESSES } from './addresses'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { MaxUint256 } from '@ethersproject/constants'; +import { Migrations } from '../migrations'; +import { getErc20Balance, move, stake } from '@/test/lib/utils'; +import { subSlippage } from '@/lib/utils/slippageHelper'; + +dotenv.config(); +const { ALCHEMY_URL: jsonRpcUrl } = process.env; + +// /* +// * Testing on GOERLI +// * - Update hardhat.config.js with chainId = 5 +// * - Update ALCHEMY_URL on .env with a goerli api key +// * - Run node on terminal: yarn run node +// * - Uncomment this section +// */ +// const network = Network.GOERLI; +// const holderAddress = '0xe0a171587b1cae546e069a943eda96916f5ee977'; // GOERLI +// const blockNumber = 7277540; + +/* + * Testing on MAINNET + * - Update hardhat.config.js with chainId = 1 + * - Update ALCHEMY_URL on .env with a mainnet api key + * - Run node on terminal: yarn run node + * - Uncomment this section + */ +const network = Network.MAINNET; +const holderAddress = '0xf346592803eb47cb8d8fa9f90b0ef17a82f877e0'; +const blockNumber = 15526452; + +const { ethers } = hardhat; +const MAX_GAS_LIMIT = 8e6; + +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); +const addresses = ADDRESSES[network]; +const fromPool = { + id: addresses.staBal3.id, + address: addresses.staBal3.address, + gauge: addresses.staBal3.gauge, +}; +const toPool = { + id: addresses.bbausd2.id, + address: addresses.bbausd2.address, + gauge: addresses.bbausd2.gauge, +}; +const { contracts } = new Contracts(network as number, provider); +const migrations = new Migrations(network); + +const relayer = addresses.relayer; + +const signRelayerApproval = async ( + relayerAddress: string, + signerAddress: string, + signer: JsonRpcSigner +): Promise => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayerAddress, true] + ); + + const signature = + await RelayerAuthorization.signSetRelayerApprovalAuthorization( + contracts.vault, + signer, + relayerAddress, + approval + ); + + const calldata = RelayerAuthorization.encodeCalldataAuthorization( + '0x', + MaxUint256, + signature + ); + + return calldata; +}; + +const reset = () => + provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber, + }, + }, + ]); + +describe('stabal3 migration execution', async () => { + let signer: JsonRpcSigner; + let signerAddress: string; + let authorisation: string; + let balance: BigNumber; + + beforeEach(async function () { + await reset(); + + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + authorisation = await signRelayerApproval(relayer, signerAddress, signer); + + // Transfer tokens from existing user account to signer + // We need that to test signatures, because hardhat doesn't have impersonated accounts private keys + balance = await move( + fromPool.address, + holderAddress, + signerAddress, + provider + ); + }); + + async function testFlow( + staked: boolean, + authorised = true, + minBptOut: undefined | string = undefined + ): Promise { + const addressIn = staked ? fromPool.gauge : fromPool.address; + const addressOut = staked ? toPool.gauge : toPool.address; + // Store balance before migration + const before = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + const amount = before.from; + + let query = migrations.stabal3( + signerAddress, + amount.toString(), + '0', + staked, + authorisation + ); + const gasLimit = MAX_GAS_LIMIT; + + // Static call can be used to simulate tx and get expected BPT in/out deltas + const staticResult = await signer.call({ + to: query.to, + data: query.data, + gasLimit, + }); + const expectedBptOut = query.decode(staticResult, staked); + const slippageAsBasisPoints = BigNumber.from('1'); // 0.01% + const expectedWithSlippage = subSlippage( + BigNumber.from(expectedBptOut), + slippageAsBasisPoints + ); + + query = migrations.stabal3( + signerAddress, + amount.toString(), + minBptOut ? minBptOut : expectedWithSlippage.toString(), + staked, + authorised ? authorisation : undefined + ); + + const response = await signer.sendTransaction({ + to: query.to, + data: query.data, + gasLimit, + }); + + const receipt = await response.wait(); + console.log('Gas used', receipt.gasUsed.toString()); + + const after = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + expect(BigNumber.from(expectedBptOut).gt(0)).to.be.true; + expect(after.from.toString()).to.eq('0'); + expect(after.to.gte(expectedWithSlippage)).to.be.true; + return expectedBptOut; + } + + let bptOut: string; + + context('staked', async () => { + beforeEach(async function () { + // Stake them + await stake(signer, fromPool.address, fromPool.gauge, balance); + }); + + it('should transfer tokens from stable to boosted', async () => { + bptOut = await testFlow(true); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(true, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('not staked', async () => { + it('should transfer tokens from stable to boosted', async () => { + bptOut = await testFlow(false); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('authorisation', async () => { + // authorisation wihtin relayer is the default case and is already tested on previous scenarios + + it('should transfer tokens from stable to boosted - pre authorised', async () => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayer, true] + ); + await signer.sendTransaction({ + to: contracts.vault.address, + data: approval, + }); + await testFlow(false, false); + }); + + it('should transfer tokens from stable to boosted - auhtorisation should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, false); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#503'); // USER_DOESNT_ALLOW_RELAYER - Relayers must be allowed by both governance and the user account + }); + }); +}); diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.ts new file mode 100644 index 000000000..32074ce5f --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/stabal3.ts @@ -0,0 +1,275 @@ +import { StablePoolEncoder } from '@/pool-stable/encoder'; +import { ADDRESSES } from './addresses'; +import { Relayer } from '@/modules/relayer/relayer.module'; +import { ExitPoolRequest } from '@/types'; +import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types'; +import { Interface } from '@ethersproject/abi'; +import { BigNumber } from '@ethersproject/bignumber'; +import { MaxInt256 } from '@ethersproject/constants'; +import { BalancerError, BalancerErrorCode } from '@/balancerErrors'; +// TODO - Ask Nico to update Typechain? +import balancerRelayerAbi from '@/lib/abi/BalancerRelayer.json'; +const balancerRelayerInterface = new Interface(balancerRelayerAbi); + +const EXIT_DAI = Relayer.toChainedReference('21'); +const EXIT_USDC = Relayer.toChainedReference('22'); +const EXIT_USDT = Relayer.toChainedReference('23'); +const SWAP_RESULT_BBAUSD = Relayer.toChainedReference('24'); + +export class StaBal3Builder { + private addresses; + + constructor(networkId: 1 | 5 | 137) { + this.addresses = ADDRESSES[networkId]; + } + + /** + * Builds migration call data. + * Migrates tokens from staBal3 to bbausd2 pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param staBal3Amount Amount of BPT tokens to migrate. + * @param minBbausd2Out Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + calldata( + userAddress: string, + staBal3Amount: string, + minBbausd2Out: string, + staked: boolean, + authorisation?: string + ): { + to: string; + data: string; + } { + if (BigNumber.from(staBal3Amount).lte(0)) + throw new BalancerError(BalancerErrorCode.INPUT_ZERO_NOT_ALLOWED); + const relayer = this.addresses.relayer; + let calls: string[] = []; + + if (authorisation) { + calls = [this.buildSetRelayerApproval(authorisation)]; + } + + if (staked) { + calls = [ + ...calls, + this.buildWithdraw(userAddress, staBal3Amount), + this.buildExit(relayer, staBal3Amount), + this.buildSwap(minBbausd2Out, relayer), + this.buildDeposit(userAddress), + ]; + } else { + calls = [ + ...calls, + this.buildExit(userAddress, staBal3Amount), + this.buildSwap(minBbausd2Out, userAddress), + ]; + } + + const callData = balancerRelayerInterface.encodeFunctionData('multicall', [ + calls, + ]); + + return { + to: relayer, + data: callData, + }; + } + + /** + * Encodes exitPool callData. + * Exit staBal3 pool proportionally to underlying stables. Exits to relayer. + * Outputreferences are used to store exit amounts for next transaction. + * + * @param sender Sender address. + * @param amount Amount of staBal3 BPT to exit with. + * @returns Encoded exitPool call. Output references. + */ + buildExit(sender: string, amount: string): string { + // Goerli and Mainnet has different assets ordering + const { assetOrder } = this.addresses.staBal3; + const assets = assetOrder.map( + (key) => this.addresses[key as keyof typeof this.addresses] as string + ); + + // Assume gaugeWithdraw returns same amount value + const userData = StablePoolEncoder.exitExactBPTInForTokensOut(amount); + // const userData = StablePoolEncoder.exitExactBPTInForOneTokenOut( + // amount, + // assetOrder.indexOf('DAI') + // ); + + // Ask to store exit outputs for batchSwap of exit is used as input to swaps + const outputReferences = [ + { index: assetOrder.indexOf('DAI'), key: EXIT_DAI }, + { index: assetOrder.indexOf('USDC'), key: EXIT_USDC }, + { index: assetOrder.indexOf('USDT'), key: EXIT_USDT }, + ]; + + const callData = Relayer.constructExitCall({ + assets, + minAmountsOut: ['0', '0', '0'], + userData, + toInternalBalance: true, + poolId: this.addresses.staBal3.id, + poolKind: 0, // This will always be 0 to match supported Relayer types + sender, + recipient: this.addresses.relayer, + outputReferences, + exitPoolRequest: {} as ExitPoolRequest, + }); + + return callData; + } + + /** + * Creates encoded batchSwap function with following swaps: stables -> linear pools -> boosted pool + * outputreferences should contain the amount of resulting BPT. + * + * @param expectedBptReturn BPT amount expected out of the swap. + * @param recipient Recipient address. + * @returns Encoded batchSwap call. Output references. + */ + buildSwap(expectedBptReturn: string, recipient: string): string { + const assets = [ + this.addresses.bbausd2.address, + this.addresses.DAI, + this.addresses.linearDai2.address, + this.addresses.USDC, + this.addresses.linearUsdc2.address, + this.addresses.USDT, + this.addresses.linearUsdt2.address, + ]; + + const outputReferences = [{ index: 0, key: SWAP_RESULT_BBAUSD }]; + + // for each linear pool swap - + // linear1Bpt[linear1]stable[linear2]linear2bpt[bbausd2]bbausd2 Uses chainedReference from previous action for amount. + // TO DO - Will swap order matter here? John to ask Fernando. + const swaps: BatchSwapStep[] = [ + { + poolId: this.addresses.linearDai2.id, + assetInIndex: 1, + assetOutIndex: 2, + amount: EXIT_DAI.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 2, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdc2.id, + assetInIndex: 3, + assetOutIndex: 4, + amount: EXIT_USDC.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 4, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + { + poolId: this.addresses.linearUsdt2.id, + assetInIndex: 5, + assetOutIndex: 6, + amount: EXIT_USDT.toString(), + userData: '0x', + }, + { + poolId: this.addresses.bbausd2.id, + assetInIndex: 6, + assetOutIndex: 0, + amount: '0', + userData: '0x', + }, + ]; + + // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number. + const limits = [ + BigNumber.from(expectedBptReturn).mul(-1).toString(), + MaxInt256.toString(), + '0', + MaxInt256.toString(), + '0', + MaxInt256.toString(), + '0', + ]; + + // Swap to/from Relayer + const funds: FundManagement = { + sender: this.addresses.relayer, + recipient, + fromInternalBalance: true, + toInternalBalance: false, + }; + + const encodedBatchSwap = Relayer.encodeBatchSwap({ + swapType: SwapType.SwapExactIn, + swaps, + assets, + funds, + limits, + deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now + value: '0', + outputReferences, + }); + + return encodedBatchSwap; + } + + /** + * Uses relayer to withdraw staked BPT from gauge and send to relayer + * + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @returns withdraw call + */ + buildWithdraw(sender: string, amount: string): string { + return Relayer.encodeGaugeWithdraw( + this.addresses.staBal3.gauge, + sender, + this.addresses.relayer, + amount + ); + } + + /** + * Uses relayer to deposit user's BPT to gauge and sends to recipient + * + * @param recipient Recipient address. + * @returns deposit call + */ + buildDeposit(recipient: string): string { + return Relayer.encodeGaugeDeposit( + this.addresses.bbausd2.gauge, + this.addresses.relayer, + recipient, + SWAP_RESULT_BBAUSD.toString() + ); + } + + /** + * Uses relayer to approve itself to act in behalf of the user + * + * @param authorisation Encoded authorisation call. + * @returns relayer approval call + */ + buildSetRelayerApproval(authorisation: string): string { + return Relayer.encodeSetRelayerApproval( + this.addresses.relayer, + true, + authorisation + ); + } +} diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/stables.integration.spec.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/stables.integration.spec.ts new file mode 100644 index 000000000..0468cb767 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/stables.integration.spec.ts @@ -0,0 +1,292 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import hardhat from 'hardhat'; +import { Network, RelayerAuthorization } from '@/.'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contracts } from '@/modules/contracts/contracts.module'; +import { ADDRESSES } from './addresses'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { MaxUint256 } from '@ethersproject/constants'; +import { Migrations } from '../migrations'; +import { getErc20Balance, move, stake } from '@/test/lib/utils'; + +dotenv.config(); + +/* + * Testing on GOERLI + * - Update hardhat.config.js with chainId = 5 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +const network = Network.GOERLI; +const addresses = ADDRESSES[network]; +const blockNumber = 7300090; +const holderAddress = '0xd86a11b0c859c18bfc1b4acd072c5afe57e79438'; +// stabal3 +const fromPool = { + id: addresses.staBal3.id, + address: addresses.staBal3.address, + gauge: addresses.staBal3.gauge, +}; +// new stabal3 +const toPool = { + id: addresses.staBal3_2.id, + address: addresses.staBal3_2.address, + gauge: addresses.staBal3_2.gauge, +}; +const tokens = [addresses.USDT, addresses.DAI, addresses.USDC]; // this order only works for testing with Goerli - change order to test on Mainnet + +/* + * Testing on POLYGON + * - Update hardhat.config.js with chainId = 137 + * - Update ALCHEMY_URL on .env with a goerli api key + * - Run node on terminal: yarn run node + * - Uncomment section below + */ +// const network = Network.POLYGON; +// const addresses = ADDRESSES[network]; +// const blockNumber = 32856000; +// const holderAddress = '0x8df33a75e5cc9d71db97fb1248cc8bdac316fe09'; +// // MaticX +// const fromPool = { +// id: '0xc17636e36398602dd37bb5d1b3a9008c7629005f0002000000000000000004c4', +// address: '0xc17636e36398602dd37bb5d1b3a9008c7629005f', +// gauge: '0x48534d027f8962692122db440714ffe88ab1fa85', +// }; +// // new MaticX +// const toPool = { +// id: '0xb20fc01d21a50d2c734c4a1262b4404d41fa7bf000000000000000000000075c', +// address: '0xb20fc01d21a50d2c734c4a1262b4404d41fa7bf0', +// gauge: '0xdffe97094394680362ec9706a759eb9366d804c2', +// }; +// const tokens = [ +// '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', // wMATIC +// '0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6', // MaticX +// ]; +// const holderAddress = '0x70d04384b5c3a466ec4d8cfb8213efc31c6a9d15'; +// // stMATIC +// const fromPool = { +// id: '0xaf5e0b5425de1f5a630a8cb5aa9d97b8141c908d000200000000000000000366', +// address: '0xaf5e0b5425de1f5a630a8cb5aa9d97b8141c908d', +// gauge: '0x9928340f9e1aaad7df1d95e27bd9a5c715202a56', +// }; +// // new stMATIC +// const toPool = { +// id: '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d', +// address: '0x8159462d255c1d24915cb51ec361f700174cd994', +// gauge: '0x2aa6fb79efe19a3fce71c46ae48efc16372ed6dd', +// }; +// const tokens = [ +// '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', // wMATIC +// '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', // stMATIC +// ]; + +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const { ethers } = hardhat; +const MAX_GAS_LIMIT = 8e6; + +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); +const relayer = addresses.relayer; +const { contracts } = new Contracts(network as number, provider); +const migrations = new Migrations(network); + +const signRelayerApproval = async ( + relayerAddress: string, + signerAddress: string, + signer: JsonRpcSigner +): Promise => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayerAddress, true] + ); + + const signature = + await RelayerAuthorization.signSetRelayerApprovalAuthorization( + contracts.vault, + signer, + relayerAddress, + approval + ); + + const calldata = RelayerAuthorization.encodeCalldataAuthorization( + '0x', + MaxUint256, + signature + ); + + return calldata; +}; + +const reset = () => + provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber, + }, + }, + ]); + +describe('stables migration execution', async () => { + let signer: JsonRpcSigner; + let signerAddress: string; + let authorisation: string; + let balance: BigNumber; + + beforeEach(async function () { + await reset(); + + signer = provider.getSigner(); + signerAddress = await signer.getAddress(); + authorisation = await signRelayerApproval(relayer, signerAddress, signer); + + // Transfer tokens from existing user account to signer + // We need that to test signatures, because hardhat doesn't have impersonated accounts private keys + balance = await move( + fromPool.address, + holderAddress, + signerAddress, + provider + ); + }); + + const testFlow = async ( + staked: boolean, + authorised = true, + minBptOut: undefined | string = undefined + ) => { + const addressIn = staked ? fromPool.gauge : fromPool.address; + const addressOut = staked ? toPool.gauge : toPool.address; + // Store balance before migration + const before = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + let query = migrations.stables( + signerAddress, + fromPool, + toPool, + before.from.toString(), + '0', + staked, + tokens, + authorisation + ); + + const gasLimit = MAX_GAS_LIMIT; + + // Static call can be used to simulate tx and get expected BPT in/out deltas + const staticResult = await signer.call({ + to: query.to, + data: query.data, + gasLimit, + }); + const bptOut = query.decode(staticResult, staked); + + query = migrations.stables( + signerAddress, + fromPool, + toPool, + before.from.toString(), + minBptOut ? minBptOut : bptOut, + staked, + tokens, + authorised ? authorisation : undefined + ); + + const response = await signer.sendTransaction({ + to: query.to, + data: query.data, + gasLimit, + }); + + const receipt = await response.wait(); + console.log('Gas used', receipt.gasUsed.toString()); + + const after = { + from: await getErc20Balance(addressIn, provider, signerAddress), + to: await getErc20Balance(addressOut, provider, signerAddress), + }; + + const diffs = { + from: after.from.sub(before.from), + to: after.to.sub(before.to), + }; + + console.log(diffs.from, diffs.to); + + expect(BigNumber.from(bptOut).gt(0)).to.be.true; + expect(after.from.toString()).to.eq('0'); + expect(after.to.toString()).to.eq(bptOut); + return bptOut; + }; + + let bptOut: string; + + context('staked', async () => { + beforeEach(async function () { + // Stake them + await stake(signer, fromPool.address, fromPool.gauge, balance); + }); + + it('should transfer tokens from stable to boosted', async () => { + bptOut = await testFlow(true); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(true, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('not staked', async () => { + it('should transfer tokens from stable to boosted', async () => { + // Store balance before migration + bptOut = await testFlow(false); + }); + + it('should transfer tokens from stable to boosted - limit should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, true, BigNumber.from(bptOut).add(1).toString()); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#507'); // SWAP_LIMIT - Swap violates user-supplied limits (min out or max in) + }); + }); + + context('authorisation', async () => { + // authorisation wihtin relayer is the default case and is already tested on previous scenarios + + it('should transfer tokens from stable to boosted - pre authorised', async () => { + const approval = contracts.vault.interface.encodeFunctionData( + 'setRelayerApproval', + [signerAddress, relayer, true] + ); + await signer.sendTransaction({ + to: contracts.vault.address, + data: approval, + }); + await testFlow(false, false); + }); + + it('should transfer tokens from stable to boosted - auhtorisation should fail', async () => { + let errorMessage = ''; + try { + await testFlow(false, false); + } catch (error) { + errorMessage = (error as Error).message; + } + expect(errorMessage).to.contain('BAL#503'); // USER_DOESNT_ALLOW_RELAYER - Relayers must be allowed by both governance and the user account + }); + }); +}); diff --git a/balancer-js/src/modules/zaps/bbausd2-migrations/stables.ts b/balancer-js/src/modules/zaps/bbausd2-migrations/stables.ts new file mode 100644 index 000000000..641f677f6 --- /dev/null +++ b/balancer-js/src/modules/zaps/bbausd2-migrations/stables.ts @@ -0,0 +1,251 @@ +import { StablePoolEncoder } from '@/pool-stable/encoder'; +import { ADDRESSES } from './addresses'; +import { Relayer } from '@/modules/relayer/relayer.module'; +import { ExitPoolRequest } from '@/types'; +import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types'; +import { Interface } from '@ethersproject/abi'; +import { BigNumber } from '@ethersproject/bignumber'; +import { MaxInt256 } from '@ethersproject/constants'; +// TODO - Ask Nico to update Typechain? +import balancerRelayerAbi from '@/lib/abi/BalancerRelayer.json'; +const balancerRelayerInterface = new Interface(balancerRelayerAbi); + +const SWAP_RESULT = Relayer.toChainedReference('0'); +const EXIT_RESULTS: BigNumber[] = []; + +export class StablesBuilder { + private addresses; + + constructor(networkId: 1 | 5 | 137) { + this.addresses = ADDRESSES[networkId]; + } + + /** + * Builds migration call data. + * Migrates tokens from old stable to new stable phantom pools with the same underlying tokens. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param from Pool info being migrated from + * @param to Pool info being migrated to + * @param bptIn Amount of BPT tokens to migrate. + * @param minBptOut Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param underlyingTokens Underlying token addresses. Array must have the same length and order as underlying tokens in pool being migrated from. Refer to [getPoolTokens](https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/vault/IVault.sol#L334). + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + calldata( + userAddress: string, + from: { id: string; address: string; gauge?: string }, + to: { id: string; address: string; gauge?: string }, + bptIn: string, + minBptOut: string, + staked: boolean, + underlyingTokens: string[], + authorisation?: string + ): { + to: string; + data: string; + } { + if (staked && (from.gauge == undefined || to.gauge == undefined)) + throw new Error( + 'Staked flow migration requires gauge addresses to be provided' + ); + + const relayer = this.addresses.relayer; + let calls: string[] = []; + + if (authorisation) { + calls = [this.buildSetRelayerApproval(authorisation)]; + } + + if (staked) { + calls = [ + ...calls, + this.buildWithdraw(userAddress, bptIn, from.gauge as string), + this.buildExit(from.id, relayer, bptIn, underlyingTokens), + this.buildSwap(minBptOut, relayer, to.id, to.address, underlyingTokens), + this.buildDeposit(userAddress, to.gauge as string), + ]; + } else { + calls = [ + ...calls, + this.buildExit(from.id, userAddress, bptIn, underlyingTokens), + this.buildSwap( + minBptOut, + userAddress, + to.id, + to.address, + underlyingTokens + ), + ]; + } + + const callData = balancerRelayerInterface.encodeFunctionData('multicall', [ + calls, + ]); + + return { + to: this.addresses.relayer, + data: callData, + }; + } + + /** + * Encodes exitPool call data. + * Exit stable pool proportionally to underlying stables. Exits to relayer. + * Outputreferences are used to store exit amounts for next transaction. + * + * @param poolId Pool id. + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @param underlyingTokens Token addresses to exit to. + * @returns Encoded exitPool call. Output references. + */ + buildExit( + poolId: string, + sender: string, + amount: string, + underlyingTokens: string[] + ): string { + // Assume gaugeWithdraw returns same amount value + const userData = StablePoolEncoder.exitExactBPTInForTokensOut(amount); + + // Store exit outputs to be used as swaps inputs + const outputReferences = []; + for (let i = 0; i < underlyingTokens.length; i++) { + outputReferences[i] = { + index: i, + key: Relayer.toChainedReference(`${i + 1}`), // index 0 will be used by swap result + }; + EXIT_RESULTS.push(outputReferences[i].key); + } + + const minAmountsOut = Array(underlyingTokens.length).fill('0'); + + const callData = Relayer.constructExitCall({ + assets: underlyingTokens, + minAmountsOut, + userData, + toInternalBalance: true, + poolId, + poolKind: 0, // This will always be 0 to match supported Relayer types + sender, + recipient: this.addresses.relayer, + outputReferences, + exitPoolRequest: {} as ExitPoolRequest, + }); + + return callData; + } + + /** + * Creates encoded batchSwap function to swap stables to new phantom stable pool BPT. + * outputreferences should contain the amount of resulting BPT. + * + * @param expectedBptReturn BPT amount expected out of the swap. + * @param recipient Recipient address. + * @param poolId Pool id + * @param poolAddress Pool address + * @param tokens Token addresses to swap from. + * @returns BatchSwap call. + */ + buildSwap( + expectedBptReturn: string, + recipient: string, + poolId: string, + poolAddress: string, + tokens: string[] + ): string { + const assets = [poolAddress, ...tokens]; + + const outputReferences = [{ index: 0, key: SWAP_RESULT }]; + + const swaps: BatchSwapStep[] = []; + // Add a swap flow for each token provided + for (let i = 0; i < tokens.length; i++) { + swaps.push({ + poolId, + assetInIndex: i + 1, + assetOutIndex: 0, + amount: EXIT_RESULTS[i].toString(), + userData: '0x', + }); + } + + // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number. + const limits = [BigNumber.from(expectedBptReturn).mul(-1).toString()]; + for (let i = 0; i < tokens.length; i++) { + limits.push(MaxInt256.toString()); + } + + // Swap to/from Relayer + const funds: FundManagement = { + sender: this.addresses.relayer, + recipient, + fromInternalBalance: true, + toInternalBalance: false, + }; + + const encodedBatchSwap = Relayer.encodeBatchSwap({ + swapType: SwapType.SwapExactIn, + swaps, + assets, + funds, + limits, + deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now + value: '0', + outputReferences, + }); + + return encodedBatchSwap; + } + + /** + * Uses relayer to withdraw staked BPT from gauge and send to relayer + * + * @param sender Sender address. + * @param amount Amount of BPT to exit with. + * @param gaugeAddress Gauge address. + * @returns withdraw call + */ + buildWithdraw(sender: string, amount: string, gaugeAddress: string): string { + return Relayer.encodeGaugeWithdraw( + gaugeAddress, + sender, + this.addresses.relayer, + amount + ); + } + + /** + * Uses relayer to deposit user's BPT to gauge and sends to recipient + * + * @param recipient Recipient address. + * @param gaugeAddress Gauge address. + * @returns deposit call + */ + buildDeposit(recipient: string, gaugeAddress: string): string { + return Relayer.encodeGaugeDeposit( + gaugeAddress, + this.addresses.relayer, + recipient, + SWAP_RESULT.toString() + ); + } + + /** + * Uses relayer to approve itself to act in behalf of the user + * + * @param authorisation Encoded authorisation call. + * @returns relayer approval call + */ + buildSetRelayerApproval(authorisation: string): string { + return Relayer.encodeSetRelayerApproval( + this.addresses.relayer, + true, + authorisation + ); + } +} diff --git a/balancer-js/src/modules/zaps/migrations.ts b/balancer-js/src/modules/zaps/migrations.ts new file mode 100644 index 000000000..29dfb9816 --- /dev/null +++ b/balancer-js/src/modules/zaps/migrations.ts @@ -0,0 +1,216 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { StaBal3Builder } from './bbausd2-migrations/stabal3'; +import { BbaUsd1Builder } from './bbausd2-migrations/bbausd1'; +import { StablesBuilder } from './bbausd2-migrations/stables'; +import { MaiusdBuilder } from './bbausd2-migrations/maiusd'; + +export class Migrations { + constructor(private network: 1 | 5 | 137) {} + + /** + * Builds migration call data. + * Migrates tokens from staBal3 to bbausd2 pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param staBal3Amount Amount of BPT tokens to migrate. + * @param minBbausd2Out Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + stabal3( + userAddress: string, + staBal3Amount: string, + minBbausd2Out: string, + staked: boolean, + authorisation?: string + ): { + to: string; + data: string; + decode: (output: string, staked: boolean) => string; + } { + const builder = new StaBal3Builder(this.network); + const request = builder.calldata( + userAddress, + staBal3Amount, + minBbausd2Out, + staked, + authorisation + ); + + return { + to: request.to, + data: request.data, + decode: (output, staked) => { + let swapIndex = staked ? 2 : 1; + if (authorisation) swapIndex += 1; + const multicallResult = defaultAbiCoder.decode(['bytes[]'], output); + const swapDeltas = defaultAbiCoder.decode( + ['int256[]'], + multicallResult[0][swapIndex] + ); + // bbausd2AmountOut + return swapDeltas[0][0].abs().toString(); + }, + }; + } + + /** + * Builds migration call data. + * Migrates tokens from bbausd1 to bbausd2 pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param bbausd1Amount Amount of BPT tokens to migrate. + * @param minBbausd2Out Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param tokenBalances Token balances in EVM scale. Array must have the same length and order as tokens in pool being migrated from. Refer to [getPoolTokens](https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/vault/IVault.sol#L334). + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + bbaUsd( + userAddress: string, + bbausd1Amount: string, + minBbausd2Out: string, + staked: boolean, + tokenBalances: string[], + authorisation?: string + ): { + to: string; + data: string; + decode: (output: string, staked: boolean) => string; + } { + const builder = new BbaUsd1Builder(this.network); + const request = builder.calldata( + userAddress, + bbausd1Amount, + minBbausd2Out, + staked, + tokenBalances, + authorisation + ); + + return { + to: request.to, + data: request.data, + decode: (output, staked) => { + let swapIndex = staked ? 1 : 0; + if (authorisation) swapIndex += 1; + const multicallResult = defaultAbiCoder.decode(['bytes[]'], output); + const swapDeltas = defaultAbiCoder.decode( + ['int256[]'], + multicallResult[0][swapIndex] + ); + return swapDeltas[0][0].abs().toString(); // bptOut + }, + }; + } + + /** + * Builds migration call data. + * Migrates tokens from old stable to new stable phantom pools with the same underlying tokens. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param from Pool info being migrated from + * @param to Pool info being migrated to + * @param bptIn Amount of BPT tokens to migrate. + * @param minBptOut Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param underlyingTokens Underlying token addresses. Array must have the same length and order as tokens in pool being migrated from. Refer to [getPoolTokens](https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/vault/IVault.sol#L334). + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + stables( + userAddress: string, + from: { id: string; address: string; gauge?: string }, + to: { id: string; address: string; gauge?: string }, + bptIn: string, + minBptOut: string, + staked: boolean, + underlyingTokens: string[], + authorisation?: string + ): { + to: string; + data: string; + decode: (output: string, staked: boolean) => string; + } { + const builder = new StablesBuilder(this.network); + const request = builder.calldata( + userAddress, + from, + to, + bptIn, + minBptOut, + staked, + underlyingTokens, + authorisation + ); + + return { + to: request.to, + data: request.data, + decode: (output, staked) => { + let swapIndex = staked ? 2 : 1; + if (authorisation) swapIndex += 1; + const multicallResult = defaultAbiCoder.decode(['bytes[]'], output); + const swapDeltas = defaultAbiCoder.decode( + ['int256[]'], + multicallResult[0][swapIndex] + ); + // bbausd2AmountOut + return swapDeltas[0][0].abs().toString(); + }, + }; + } + + /** + * Builds migration call data. + * Migrates tokens from staBal3 to bbausd2 pool. + * Tokens that are initially staked are re-staked at the end of migration. Non-staked are not. + * + * @param userAddress User address. + * @param bptIn Amount of BPT tokens to migrate. + * @param minBptOut Minimum of expected BPT out ot the migration flow. + * @param staked Indicates whether tokens are initially staked or not. + * @param authorisation Encoded authorisation call. + * @returns Migration transaction request ready to send with signer.sendTransaction + */ + maiusd( + userAddress: string, + bptIn: string, + minBptOut: string, + staked: boolean, + authorisation?: string + ): { + to: string; + data: string; + decode: (output: string, staked: boolean) => string; + } { + const builder = new MaiusdBuilder(this.network); + const request = builder.calldata( + userAddress, + bptIn, + minBptOut, + staked, + authorisation + ); + + return { + to: request.to, + data: request.data, + decode: (output, staked) => { + let swapIndex = staked ? 2 : 1; + if (authorisation) swapIndex += 1; + const multicallResult = defaultAbiCoder.decode(['bytes[]'], output); + const swapDeltas = defaultAbiCoder.decode( + ['int256[]'], + multicallResult[0][swapIndex] + ); + const bptOut = swapDeltas[0][8].abs().toString(); + return bptOut; + }, + }; + } +} diff --git a/balancer-js/src/modules/zaps/zaps.module.spec.ts b/balancer-js/src/modules/zaps/zaps.module.spec.ts new file mode 100644 index 000000000..f6cf6fe68 --- /dev/null +++ b/balancer-js/src/modules/zaps/zaps.module.spec.ts @@ -0,0 +1,26 @@ +import dotenv from 'dotenv'; +import { expect } from 'chai'; +import { BalancerSdkConfig, Network, BalancerSDK, Relayer } from '@/.'; +import { Zaps } from './zaps.module'; + +dotenv.config(); + +const sdkConfig: BalancerSdkConfig = { + network: Network.MAINNET, + rpcUrl: `https://mainnet.infura.io/v3/${process.env.INFURA}`, +}; + +describe('zaps module', () => { + context('instantiation', () => { + it('instantiate via module', async () => { + const relayer = new Relayer(sdkConfig); + const zaps = new Zaps(Network.MAINNET, relayer); + expect(zaps.network).to.deep.eq(Network.MAINNET); + }); + + it('instantiate via SDK', async () => { + const balancer = new BalancerSDK(sdkConfig); + expect(balancer.zaps.network).to.deep.eq(Network.MAINNET); + }); + }); +}); diff --git a/balancer-js/src/modules/zaps/zaps.module.ts b/balancer-js/src/modules/zaps/zaps.module.ts new file mode 100644 index 000000000..66a603d22 --- /dev/null +++ b/balancer-js/src/modules/zaps/zaps.module.ts @@ -0,0 +1,10 @@ +import { Network } from '@/lib/constants/network'; +import { Migrations } from './migrations'; + +export class Zaps { + public migrations: Migrations; + + constructor(public network: Network) { + this.migrations = new Migrations(network as 1 | 5); + } +} diff --git a/balancer-js/src/pool-composable-stable/encoder.ts b/balancer-js/src/pool-composable-stable/encoder.ts new file mode 100644 index 000000000..f8c71f5d0 --- /dev/null +++ b/balancer-js/src/pool-composable-stable/encoder.ts @@ -0,0 +1,114 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { StablePhantomPoolJoinKind } from '../pool-stable/index'; + +export enum ComposableStablePoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, +} + +export enum ComposableStablePoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + BPT_IN_FOR_EXACT_TOKENS_OUT, +} + +export class ComposableStablePoolEncoder { + /** + * Cannot be constructed. + */ + private constructor() { + // eslint-disable-next-line @typescript-eslint/no-empty-function + } + + /** + * Encodes the userData parameter for providing the initial liquidity to a ComposableStablePool + * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances + */ + static joinInit = (amountsIn: BigNumberish[]): string => + defaultAbiCoder.encode( + ['uint256', 'uint256[]'], + [ComposableStablePoolJoinKind.INIT, amountsIn] + ); + + /** + * Encodes the userData parameter for collecting protocol fees for StablePhantomPool + */ + static joinCollectProtocolFees = (): string => + defaultAbiCoder.encode( + ['uint256'], + [StablePhantomPoolJoinKind.COLLECT_PROTOCOL_FEES] + ); + + /** + * Encodes the userData parameter for joining a ComposableStablePool with exact token inputs + * @param amountsIn - the amounts each of token to deposit in the pool as liquidity + * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens + */ + static joinExactTokensInForBPTOut = ( + amountsIn: BigNumberish[], + minimumBPT: BigNumberish + ): string => + defaultAbiCoder.encode( + ['uint256', 'uint256[]', 'uint256'], + [ + ComposableStablePoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + amountsIn, + minimumBPT, + ] + ); + + /** + * Encodes the userData parameter for joining a ComposableStablePool with to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + * @param enterTokenIndex - the index of the token to be provided as liquidity + */ + static joinTokenInForExactBPTOut = ( + bptAmountOut: BigNumberish, + enterTokenIndex: number + ): string => + defaultAbiCoder.encode( + ['uint256', 'uint256', 'uint256'], + [ + ComposableStablePoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, + bptAmountOut, + enterTokenIndex, + ] + ); + + /** + * Encodes the userData parameter for exiting a ComposableStablePool by removing a single token in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + * @param enterTokenIndex - the index of the token to removed from the pool + */ + static exitExactBPTInForOneTokenOut = ( + bptAmountIn: BigNumberish, + exitTokenIndex: number + ): string => + defaultAbiCoder.encode( + ['uint256', 'uint256', 'uint256'], + [ + ComposableStablePoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + bptAmountIn, + exitTokenIndex, + ] + ); + + /** + * Encodes the userData parameter for exiting a ComposableStablePool by removing exact amounts of tokens + * @param amountsOut - the amounts of each token to be withdrawn from the pool + * @param maxBPTAmountIn - the minimum acceptable BPT to burn in return for withdrawn tokens + */ + static exitBPTInForExactTokensOut = ( + amountsOut: BigNumberish[], + maxBPTAmountIn: BigNumberish + ): string => + defaultAbiCoder.encode( + ['uint256', 'uint256[]', 'uint256'], + [ + ComposableStablePoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, + amountsOut, + maxBPTAmountIn, + ] + ); +} diff --git a/balancer-js/src/pool-composable-stable/index.ts b/balancer-js/src/pool-composable-stable/index.ts new file mode 100644 index 000000000..ff34a8a27 --- /dev/null +++ b/balancer-js/src/pool-composable-stable/index.ts @@ -0,0 +1 @@ +export * from './encoder'; diff --git a/balancer-js/src/test/lib/utils.ts b/balancer-js/src/test/lib/utils.ts index c991b203e..69883d950 100644 --- a/balancer-js/src/test/lib/utils.ts +++ b/balancer-js/src/test/lib/utils.ts @@ -1,10 +1,16 @@ import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { BigNumber } from '@ethersproject/bignumber'; -import { AddressZero } from '@ethersproject/constants'; +import { AddressZero, MaxUint256 } from '@ethersproject/constants'; import { balancerVault } from '@/lib/constants/config'; import { hexlify, zeroPad } from '@ethersproject/bytes'; import { keccak256 } from '@ethersproject/solidity'; +import { parseEther } from '@ethersproject/units'; import { ERC20 } from '@/modules/contracts/ERC20'; +import { setBalance } from '@nomicfoundation/hardhat-network-helpers'; + +import { Interface } from '@ethersproject/abi'; +const liquidityGaugeAbi = ['function deposit(uint value) payable']; +const liquidityGauge = new Interface(liquidityGaugeAbi); import { Pools as PoolsProvider } from '@/modules/pools'; import { PoolWithMethods, BalancerError, BalancerErrorCode } from '@/.'; @@ -24,7 +30,8 @@ export const forkSetup = async ( slots: number[], balances: string[], jsonRpcUrl: string, - blockNumber?: number + blockNumber?: number, + isVyperMapping = false ): Promise => { await signer.provider.send('hardhat_reset', [ { @@ -37,7 +44,14 @@ export const forkSetup = async ( for (let i = 0; i < tokens.length; i++) { // Set initial account balance for each token that will be used to join pool - await setTokenBalance(signer, tokens[i], slots[i], balances[i]); + await setTokenBalance( + signer, + tokens[i], + slots[i], + balances[i], + isVyperMapping + ); + // Approve appropriate allowances so that vault contract can move tokens await approveToken(tokens[i], balances[i], signer); } @@ -55,7 +69,8 @@ export const setTokenBalance = async ( signer: JsonRpcSigner, token: string, slot: number, - balance: string + balance: string, + isVyperMapping = false ): Promise => { const toBytes32 = (bn: BigNumber) => { return hexlify(zeroPad(bn.toHexString(), 32)); @@ -69,10 +84,18 @@ export const setTokenBalance = async ( const signerAddress = await signer.getAddress(); // Get storage slot index - const index = keccak256( - ['uint256', 'uint256'], - [signerAddress, slot] // key, slot - ); + let index; + if (isVyperMapping) { + index = keccak256( + ['uint256', 'uint256'], + [slot, signerAddress] // slot, key + ); + } else { + index = keccak256( + ['uint256', 'uint256'], + [signerAddress, slot] // key, slot + ); + } // Manipulate local balance (needs to be bytes32 string) await setStorageAt( @@ -128,3 +151,49 @@ export const getBalances = async ( } return Promise.all(balances); }; + +export const move = async ( + token: string, + from: string, + to: string, + provider: JsonRpcProvider +): Promise => { + const holder = await impersonateAccount(from, provider); + const balance = await getErc20Balance(token, provider, from); + await ERC20(token, provider).connect(holder).transfer(to, balance); + + return balance; +}; + +// https://hardhat.org/hardhat-network/docs/guides/forking-other-networks#impersonating-accounts +// WARNING: don't use hardhat SignerWithAddress to sendTransactions!! +// It's not working and we didn't have time to figure out why. +// Use JsonRpcSigner instead +export const impersonateAccount = async ( + account: string, + provider: JsonRpcProvider +): Promise => { + await provider.send('hardhat_impersonateAccount', [account]); + await setBalance(account, parseEther('10000')); + return provider.getSigner(account); +}; + +export const stake = async ( + signer: JsonRpcSigner, + pool: string, + gauge: string, + balance: BigNumber +): Promise => { + await ( + await ERC20(pool, signer.provider) + .connect(signer) + .approve(gauge, MaxUint256) + ).wait(); + + await ( + await signer.sendTransaction({ + to: gauge, + data: liquidityGauge.encodeFunctionData('deposit', [balance]), + }) + ).wait(); +}; diff --git a/balancer-js/src/types.ts b/balancer-js/src/types.ts index 35281359b..0c7a3bea6 100644 --- a/balancer-js/src/types.ts +++ b/balancer-js/src/types.ts @@ -196,6 +196,7 @@ export enum PoolType { Weighted = 'Weighted', Investment = 'Investment', Stable = 'Stable', + ComposableStable = 'ComposableStable', MetaStable = 'MetaStable', StablePhantom = 'StablePhantom', LiquidityBootstrapping = 'LiquidityBootstrapping', diff --git a/balancer-js/yarn.lock b/balancer-js/yarn.lock index 14ff320d6..de065ea0f 100644 --- a/balancer-js/yarn.lock +++ b/balancer-js/yarn.lock @@ -1450,6 +1450,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/hardhat-network-helpers@^1.0.4": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.6.tgz#5e2026ddde5ca539f70a2bf498528afd08bd0827" + integrity sha512-a35iVD4ycF6AoTfllAnKm96IPIzzHpgKX/ep4oKc2bsUKFfMlacWdyntgC/7d5blyCTXfFssgNAvXDZfzNWVGQ== + dependencies: + ethereumjs-util "^7.1.4" + "@nomiclabs/hardhat-ethers@^2.0.5": version "2.1.1" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.1.1.tgz#3f1d1ab49813d1bae4c035cc1adec224711e528b"