From 64b3970bcf45c5c4a6c8f30cc23238bb0de61b29 Mon Sep 17 00:00:00 2001 From: scolear Date: Wed, 2 Oct 2024 08:57:23 +0200 Subject: [PATCH 01/27] wip: poolmerchant --- contracts/DLCManager.sol | 35 ++++++++++++++++-- contracts/PoolMerchant.sol | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 contracts/PoolMerchant.sol diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index 37ce286..c098ffe 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -205,10 +205,10 @@ contract DLCManager is // INTERNAL FUNCTIONS // //////////////////////////////////////////////////////////////// - function _generateUUID( + function generateUUID( address sender, uint256 nonce - ) private view returns (bytes32) { + ) public view returns (bytes32) { return keccak256( abi.encodePacked( @@ -330,7 +330,7 @@ contract DLCManager is onlyWhitelisted returns (bytes32) { - bytes32 _uuid = _generateUUID(msg.sender, _index); + bytes32 _uuid = generateUUID(msg.sender, _index); dlcs[_index] = DLCLink.DLC({ uuid: _uuid, @@ -358,6 +358,35 @@ contract DLCManager is return _uuid; } + function setupPendingVault( + bytes32 _uuid, + string calldata _taprootPubKey, + string calldata _wdTxId + ) public onlyWhitelisted whenNotPaused { + dlcs[_index] = DLCLink.DLC({ + uuid: _uuid, + protocolContract: msg.sender, // deprecated + valueLocked: 0, + valueMinted: 0, + timestamp: block.timestamp, + creator: msg.sender, + status: DLCLink.DLCStatus.AUX_STATE_1, + fundingTxId: "", + closingTxId: "", + wdTxId: _wdTxId, + btcFeeRecipient: btcFeeRecipient, + btcMintFeeBasisPoints: btcMintFeeRate, + btcRedeemFeeBasisPoints: btcRedeemFeeRate, + taprootPubKey: _taprootPubKey + }); + + emit CreateDLC(_uuid, msg.sender, block.timestamp); + + dlcIDsByUUID[_uuid] = _index; + userVaults[msg.sender].push(_uuid); + _index++; + } + /** * @notice Confirms that a DLC was 'funded' on the Bitcoin blockchain. * @dev Called by the Attestor Coordinator. diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol new file mode 100644 index 0000000..9384f75 --- /dev/null +++ b/contracts/PoolMerchant.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// ___ __ ___ __ _ _ +// / \/ / / __\ / /(_)_ __ | | __ +// / /\ / / / / / / | | '_ \| |/ / +// / /_// /__/ /____/ /__| | | | | < +// /___,'\____|____(_)____/_|_| |_|_|\_\ + +pragma solidity 0.8.18; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "./DLCLinkLibrary.sol"; +import "./DLCManager.sol"; + +contract PoolMerchant is + Initializable, + AccessControlDefaultAdminRulesUpgradeable, + PausableUpgradeable +{ + using DLCLink for DLCLink.DLC; + using Strings for string; + + bytes32 public constant DLC_ADMIN_ROLE = + 0x2bf88000669ee6f7a648a231f4adbc117f5a8e34f980c08420b9b9a9f2640aa1; // keccak256("DLC_ADMIN_ROLE") + + DLCManager public dlcManager; + uint256 private _nonce; + mapping(bytes32 => string) public uuidToTaprootPubkey; + + function initialize( + address defaultAdmin, + address dlcAdminRole, + DLCManager dlcManagerContract + ) public initializer { + __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); + _grantRole(DLC_ADMIN_ROLE, dlcAdminRole); + dlcManager = dlcManagerContract; + _nonce = 0; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + error VaultAlreadyExists(bytes32 uuid); + + function getNewUUID(address userAddress) public view returns (bytes32) { + return dlcManager.generateUUID(userAddress, block.timestamp); + } + + // TODO: auth + function createPendingVault( + bytes32 uuid, + string memory taprootPubkey, + string calldata wdTxId + ) public { + if (uuidToTaprootPubkey[uuid].equal(taprootPubkey)) { + revert VaultAlreadyExists(uuid); + } + uuidToTaprootPubkey[uuid] = taprootPubkey; + dlcManager.setupPendingVault(uuid, taprootPubkey, wdTxId); + } + + function getSharesForUUID(bytes32 uuid) public view returns (uint256) { + DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); + uint256 shares = _dlc.valueMinted; + return shares; + } + + // sweepDeposit() { + // loop vaults and perform deposit for vaults where there is new shares + //} +} From 88812a96bd8e7da519e10cdafef4fa421662ce8c Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 3 Oct 2024 18:19:08 +0200 Subject: [PATCH 02/27] wip: enzyme-forks --- contracts/DLCManager.sol | 1 + contracts/PoolMerchant.sol | 100 +++++++++++++++++- scripts/enzyme-tests.js | 183 +++++++++++++++++++++++++++++++++ scripts/helpers/local-setup.sh | 2 +- 4 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 scripts/enzyme-tests.js diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index c098ffe..0343762 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -358,6 +358,7 @@ contract DLCManager is return _uuid; } + // TODO: auth function setupPendingVault( bytes32 _uuid, string calldata _taprootPubKey, diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 9384f75..253c57b 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -11,9 +11,48 @@ import "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRule import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./DLCLinkLibrary.sol"; import "./DLCManager.sol"; +// interface IEnzymeVault { +// function buyShares( +// uint256 _investmentAmount, +// uint256 _minSharesQuantity +// ) external returns (uint256); +// } + +interface IGlobalConfigLibComptrollerV4 { + function buyShares( + uint256 _investmentAmount, + uint256 _minSharesQuantity + ) external returns (uint256 sharesReceived_); + + function getDenominationAsset() + external + view + returns (address denominationAsset_); + + function redeemSharesForSpecificAssets( + address _recipient, + uint256 _sharesQuantity, + address[] calldata _payoutAssets, + uint256[] calldata _payoutAssetPercentages + ) external returns (uint256[] memory payoutAmounts_); + + function redeemSharesInKind( + address _recipient, + uint256 _sharesQuantity, + address[] calldata _additionalAssets, + address[] calldata _assetsToSkip + ) + external + returns ( + address[] memory payoutAssets_, + uint256[] memory payoutAmounts_ + ); +} + contract PoolMerchant is Initializable, AccessControlDefaultAdminRulesUpgradeable, @@ -21,22 +60,31 @@ contract PoolMerchant is { using DLCLink for DLCLink.DLC; using Strings for string; + using SafeERC20 for IERC20; bytes32 public constant DLC_ADMIN_ROLE = 0x2bf88000669ee6f7a648a231f4adbc117f5a8e34f980c08420b9b9a9f2640aa1; // keccak256("DLC_ADMIN_ROLE") + IGlobalConfigLibComptrollerV4 public enzymeVault; DLCManager public dlcManager; + IERC20 public dlcBTC; uint256 private _nonce; mapping(bytes32 => string) public uuidToTaprootPubkey; + mapping(bytes32 => address) public uuidToUserAddress; + mapping(bytes32 => uint256) public sweptAmounts; function initialize( address defaultAdmin, address dlcAdminRole, - DLCManager dlcManagerContract + address dlcManagerContract, + address dlcBTCContract, + address enzymeVaultContract ) public initializer { __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); _grantRole(DLC_ADMIN_ROLE, dlcAdminRole); - dlcManager = dlcManagerContract; + dlcManager = DLCManager(dlcManagerContract); + dlcBTC = IERC20(dlcBTCContract); + enzymeVault = IGlobalConfigLibComptrollerV4(enzymeVaultContract); _nonce = 0; } @@ -52,7 +100,9 @@ contract PoolMerchant is } // TODO: auth + // called by attestors function createPendingVault( + address userAddress, bytes32 uuid, string memory taprootPubkey, string calldata wdTxId @@ -61,6 +111,7 @@ contract PoolMerchant is revert VaultAlreadyExists(uuid); } uuidToTaprootPubkey[uuid] = taprootPubkey; + uuidToUserAddress[uuid] = userAddress; dlcManager.setupPendingVault(uuid, taprootPubkey, wdTxId); } @@ -70,7 +121,46 @@ contract PoolMerchant is return shares; } - // sweepDeposit() { - // loop vaults and perform deposit for vaults where there is new shares - //} + function getSweptAmountForUUID(bytes32 uuid) public view returns (uint256) { + return sweptAmounts[uuid]; + } + + // called by attestors + function sweepDeposit() public { + DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( + address(this) + ); + + for (uint256 i = 0; i < _allDLCs.length; i++) { + bytes32 uuid = _allDLCs[i].uuid; + uint256 currentValueMinted = _allDLCs[i].valueMinted; + uint256 sweptAmount = sweptAmounts[uuid]; + + if (currentValueMinted > sweptAmount) { + uint256 difference = currentValueMinted - sweptAmount; + + // Approve the Enzyme vault to spend dlcBTC tokens + dlcBTC.approve(address(enzymeVault), difference); + + // Simulate the call to buyShares to get the _minSharesQuantity + (bool success, bytes memory result) = address(enzymeVault) + .staticcall( + abi.encodeWithSignature( + "buyShares(uint256,uint256)", + difference, + 1 + ) + ); + + require(success, "Static call to buyShares failed"); + + uint256 minSharesQuantity = abi.decode(result, (uint256)); + + // Actual call to buyShares with the obtained minSharesQuantity + enzymeVault.buyShares(difference, minSharesQuantity); + + sweptAmounts[uuid] = currentValueMinted; // Update the local tracker + } + } + } } diff --git a/scripts/enzyme-tests.js b/scripts/enzyme-tests.js new file mode 100644 index 0000000..514346c --- /dev/null +++ b/scripts/enzyme-tests.js @@ -0,0 +1,183 @@ +const hardhat = require('hardhat'); +const { + loadDeploymentInfo, +} = require('./helpers/deployment-handlers_versioned'); +const callManagerContractFunction = require('./helpers/00-call-dlc-manager-fn'); + +const { getSignatures, setSigners } = require('../test/utils'); + +// This script requires forking Arbitrum Mainnet from at least block 260000776 +async function main() { + await hardhat.run('compile'); + const accounts = await hardhat.ethers.getSigners(); + let attestor1, attestor2, attestor3; + let attestors; + attestor1 = accounts[6]; + attestor2 = accounts[7]; + attestor3 = accounts[8]; + attestors = [attestor1, attestor2, attestor3]; + let btcFeeRecipient = '0x000001'; + + const dlcAdminSafe = await hardhat.ethers.getImpersonatedSigner( + '0xaA2949C5285C2f2887ABD567865344240c29d619' + ); + const coordinator = await hardhat.ethers.getImpersonatedSigner( + '0x3355977947F84C2b1CAE7D2903a72958aEE185e2' + ); + const enzymeManager = await hardhat.ethers.getImpersonatedSigner( + '0x0DD4f29E21F10cb2E485cf9bDAb9F2dD1f240Bfa' + ); + + const admin = accounts[0]; + const userAddress = accounts[1].address; + + const deployInfoManager = await loadDeploymentInfo( + hardhat.network.name, + 'DLCManager' + ); + const dlcManagerExisting = new hardhat.ethers.Contract( + '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', + deployInfoManager.contract.abi, + admin + ); + + const deployInfoToken = await loadDeploymentInfo( + hardhat.network.name, + 'DLCBTC' + ); + + const dlcBTC = new hardhat.ethers.Contract( + '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', + deployInfoToken.contract.abi, + admin + ); + + const DLCManager = await hardhat.ethers.getContractFactory('DLCManager'); + const dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + 2, + dlcBTC.address, + btcFeeRecipient, + ]); + await dlcManager.deployed(); + await setSigners(dlcManager, attestors); + + const txTranferOwnership = await dlcManagerExisting + .connect(dlcAdminSafe) + .transferTokenContractOwnership(dlcManager.address); + await txTranferOwnership.wait(); + + console.log('dlcManager:', dlcManager.address); + console.log('dlcBTC:', dlcBTC.address); + console.log('dlcBTC owner:', await dlcBTC.owner()); + + const PoolMerchant = + await hardhat.ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await hardhat.upgrades.deployProxy(PoolMerchant, [ + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + dlcManager.address, + dlcBTC.address, + '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee', + ]); + await poolMerchant.deployed(); + + const pm = new hardhat.ethers.Contract( + poolMerchant.address, + // '0x8d6e07a6c15e1cdb881d665e0da4d7c2004a1929', + // ['function getNewUUID(address) view returns (bytes32)'], + poolMerchant.interface, + admin + ); + console.log('pm:', pm.address); + const uuid = await pm.getNewUUID(admin.address); + console.log('uuid:', uuid); + + const enzymeVault = new hardhat.ethers.Contract( + '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee', + [ + 'function buyShares(uint256, uint256) external returns (uint256)', + 'function getDenominationAsset() external view returns (address)', + 'function vaultCallOnContract(address,bytes4,bytes) external', + ], + admin + ); + const asset = await enzymeVault.getDenominationAsset(); + console.log('asset:', asset); + + await enzymeVault + .connect(enzymeManager) + .vaultCallOnContract( + '0x2C6bef68DAbf0494bB5F727E63c8FB54f7D2c287', + '0x8da3d736', + '0x000000000000000000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f96c190e181b38c840b7832bba9e8d527250a5fb' + ); + + // set up pending vault + // set it to funded + // PM should have the funds + // call sweep + // enzyme shoud have the funds + + const fakeWdTxId = 'fakeWdTxId'; + const fakeTapPK = 'fakeTapPK'; + const deposit = 100000000; + + await dlcManager.connect(admin).whitelistAddress(pm.address); + console.log('whitelisted pm'); + await pm + .connect(admin) + .createPendingVault(userAddress, uuid, fakeTapPK, fakeWdTxId); + console.log('created pending vault'); + const dlc = await dlcManager.getDLC(uuid); + console.log('dlc:', dlc); + const pmBalanceBefore = await dlcBTC.balanceOf(pm.address); + + const signatureBytesForFunding = await getSignatures( + { + uuid, + btcTxId: fakeWdTxId, + functionString: 'set-status-funded', + newLockedAmount: deposit, + }, + attestors, + 3 + ); + const tx3 = await dlcManager + .connect(attestor1) + .setStatusFunded(uuid, fakeWdTxId, signatureBytesForFunding, deposit); + await tx3.wait(); + + const pmBalanceAfter = await dlcBTC.balanceOf(pm.address); + console.log('pmBalanceBefore:', pmBalanceBefore.toString()); + console.log('pmBalanceAfter:', pmBalanceAfter.toString()); + + console.log( + 'pmSweptBalanceForUUIDBefore:', + await pm.getSweptAmountForUUID(uuid) + ); + + const txSweep = await pm.connect(admin).sweepDeposit(); + await txSweep.wait(); + + console.log( + 'pmSweptBalanceForUUIDAfter:', + await pm.getSweptAmountForUUID(uuid) + ); + const pmBalanceAfterSweep = await dlcBTC.balanceOf(pm.address); + console.log('pmBalanceAfterSweep:', pmBalanceAfterSweep.toString()); + const enzymeBalance = await dlcBTC.balanceOf( + '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee' + ); + console.log('enzymeBalance:', enzymeBalance.toString()); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + +// diff --git a/scripts/helpers/local-setup.sh b/scripts/helpers/local-setup.sh index 53392f0..dcb2cee 100755 --- a/scripts/helpers/local-setup.sh +++ b/scripts/helpers/local-setup.sh @@ -10,4 +10,4 @@ dlc-link-eth add-signer 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC dlc-link-eth set-whitelisting 'false' # Vault setup and print dlcUUID -dlc-link-eth setup-vault | grep dlcUUID +dlc-link-eth setup-vault | grep uuid From 8178d36f01413e495ac9decbb2fab698dc3462cc Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 4 Oct 2024 17:24:45 +0200 Subject: [PATCH 03/27] wip: working deposit flow, early withdraw --- contracts/PoolMerchant.sol | 80 ++++++++++++---- scripts/enzyme-tests.js | 190 +++++++++++++++++++++++++++++-------- 2 files changed, 214 insertions(+), 56 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 253c57b..56f37d0 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -142,25 +142,73 @@ contract PoolMerchant is // Approve the Enzyme vault to spend dlcBTC tokens dlcBTC.approve(address(enzymeVault), difference); - // Simulate the call to buyShares to get the _minSharesQuantity - (bool success, bytes memory result) = address(enzymeVault) - .staticcall( - abi.encodeWithSignature( - "buyShares(uint256,uint256)", - difference, - 1 - ) - ); - - require(success, "Static call to buyShares failed"); - - uint256 minSharesQuantity = abi.decode(result, (uint256)); - - // Actual call to buyShares with the obtained minSharesQuantity - enzymeVault.buyShares(difference, minSharesQuantity); + enzymeVault.buyShares(difference, 1); sweptAmounts[uuid] = currentValueMinted; // Update the local tracker } } } + + // attestors call this + function withdraw( + bytes32 uuid, + uint256 amount, + string memory taprootPubkey, + string calldata /*wdTxId*/ + ) public { + require( + keccak256(abi.encodePacked(uuidToTaprootPubkey[uuid])) == + keccak256(abi.encodePacked(taprootPubkey)), + "Invalid taproot pubkey" + ); + + address[] memory payoutAssets = new address[](1); + payoutAssets[0] = address(dlcBTC); + uint256[] memory payoutPercentage = new uint256[](1); + payoutPercentage[0] = 10000; + + enzymeVault.redeemSharesForSpecificAssets( + address(this), + amount, + payoutAssets, + payoutPercentage + ); + + dlcManager.withdraw(uuid, amount); + DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); + sweptAmounts[uuid] = _dlc.valueMinted; // Update the local tracker + } + + // function sweepRedeem() public { + // DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( + // address(this) + // ); + + // for (uint256 i = 0; i < _allDLCs.length; i++) { + // bytes32 uuid = _allDLCs[i].uuid; + // uint256 currentValueMinted = _allDLCs[i].valueMinted; + // uint256 sweptAmount = sweptAmounts[uuid]; + + // if (currentValueMinted < sweptAmount) { + // uint256 difference = sweptAmount - currentValueMinted; + + // // Approve the Enzyme vault to spend dlcBTC tokens + // // dlcBTC.approve(address(enzymeVault), difference); + + // address[] memory payoutAssets = new address[](1); + // payoutAssets[0] = address(dlcBTC); + // uint256[] memory payoutPercentage = new uint256[](1); + // payoutPercentage[0] = 100; + + // enzymeVault.redeemSharesForSpecificAssets( + // address(this), + // difference, + // payoutAssets, + // payoutPercentage + // ); + + // sweptAmounts[uuid] = currentValueMinted; // Update the local tracker + // } + // } + // } } diff --git a/scripts/enzyme-tests.js b/scripts/enzyme-tests.js index 514346c..8d1b424 100644 --- a/scripts/enzyme-tests.js +++ b/scripts/enzyme-tests.js @@ -28,6 +28,9 @@ async function main() { '0x0DD4f29E21F10cb2E485cf9bDAb9F2dD1f240Bfa' ); + const enzymeComptrollerAddress = + '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee'; + const enzymeVaultAddress = '0xe1f20e9855b2bce29f92579ba193230389795b46'; const admin = accounts[0]; const userAddress = accounts[1].address; @@ -51,37 +54,66 @@ async function main() { deployInfoToken.contract.abi, admin ); + let dlcManager; - const DLCManager = await hardhat.ethers.getContractFactory('DLCManager'); - const dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - 2, - dlcBTC.address, - btcFeeRecipient, - ]); - await dlcManager.deployed(); - await setSigners(dlcManager, attestors); + const dlcManagerExists = await hardhat.ethers.getContractAt( + 'DLCManager', + '0xe4097Ee7b1AE8a7E5DdCaD820Ca31ee4d2d9Ef92', + admin + ); + + // console.log('dlcManagerExists:', dlcManagerExists); - const txTranferOwnership = await dlcManagerExisting - .connect(dlcAdminSafe) - .transferTokenContractOwnership(dlcManager.address); - await txTranferOwnership.wait(); + // TODO: this check doesnt really work + if (process.env.FORCE_DEPLOY) { + const DLCManager = + await hardhat.ethers.getContractFactory('DLCManager'); + dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + 2, + dlcBTC.address, + btcFeeRecipient, + ]); + await dlcManager.deployed(); + await setSigners(dlcManager, attestors); + + const txTranferOwnership = await dlcManagerExisting + .connect(dlcAdminSafe) + .transferTokenContractOwnership(dlcManager.address); + await txTranferOwnership.wait(); + } else { + dlcManager = dlcManagerExists; + } console.log('dlcManager:', dlcManager.address); console.log('dlcBTC:', dlcBTC.address); console.log('dlcBTC owner:', await dlcBTC.owner()); - const PoolMerchant = - await hardhat.ethers.getContractFactory('PoolMerchant'); - const poolMerchant = await hardhat.upgrades.deployProxy(PoolMerchant, [ - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - dlcManager.address, - dlcBTC.address, - '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee', - ]); - await poolMerchant.deployed(); + let poolMerchant; + + const pmExists = await hardhat.ethers.getContractAt( + 'PoolMerchant', + '0xf96C190E181b38c840B7832BbA9E8D527250a5FB', + admin + ); + + // console.log('pmExists:', pmExists); + + if (process.env.FORCE_DEPLOY) { + const PoolMerchant = + await hardhat.ethers.getContractFactory('PoolMerchant'); + poolMerchant = await hardhat.upgrades.deployProxy(PoolMerchant, [ + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + dlcManager.address, + dlcBTC.address, + enzymeComptrollerAddress, + ]); + await poolMerchant.deployed(); + } else { + poolMerchant = pmExists; + } const pm = new hardhat.ethers.Contract( poolMerchant.address, @@ -95,7 +127,7 @@ async function main() { console.log('uuid:', uuid); const enzymeVault = new hardhat.ethers.Contract( - '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee', + enzymeComptrollerAddress, [ 'function buyShares(uint256, uint256) external returns (uint256)', 'function getDenominationAsset() external view returns (address)', @@ -104,7 +136,7 @@ async function main() { admin ); const asset = await enzymeVault.getDenominationAsset(); - console.log('asset:', asset); + console.log('asset on enzyme:', asset); await enzymeVault .connect(enzymeManager) @@ -124,15 +156,20 @@ async function main() { const fakeTapPK = 'fakeTapPK'; const deposit = 100000000; - await dlcManager.connect(admin).whitelistAddress(pm.address); - console.log('whitelisted pm'); + try { + await dlcManager.connect(admin).whitelistAddress(pm.address); + console.log('whitelisted pm on dlcManager'); + } catch (error) { + console.error('error whitelisting pm on dlcManager:', error); + } await pm .connect(admin) .createPendingVault(userAddress, uuid, fakeTapPK, fakeWdTxId); - console.log('created pending vault'); + console.log('created pending vault through pm'); const dlc = await dlcManager.getDLC(uuid); - console.log('dlc:', dlc); + // console.log('dlc:', dlc); const pmBalanceBefore = await dlcBTC.balanceOf(pm.address); + console.log('pmBalanceBefore Funded:', pmBalanceBefore.toString()); const signatureBytesForFunding = await getSignatures( { @@ -144,33 +181,106 @@ async function main() { attestors, 3 ); - const tx3 = await dlcManager + const tx2 = await dlcManager .connect(attestor1) .setStatusFunded(uuid, fakeWdTxId, signatureBytesForFunding, deposit); - await tx3.wait(); + await tx2.wait(); const pmBalanceAfter = await dlcBTC.balanceOf(pm.address); - console.log('pmBalanceBefore:', pmBalanceBefore.toString()); - console.log('pmBalanceAfter:', pmBalanceAfter.toString()); + console.log('pmBalanceAfter Funded:', pmBalanceAfter.toString()); console.log( 'pmSweptBalanceForUUIDBefore:', - await pm.getSweptAmountForUUID(uuid) + (await pm.getSweptAmountForUUID(uuid)).toString() ); + const enzymeVaultBalanceBefore = await dlcBTC.balanceOf(enzymeVaultAddress); + console.log( + 'enzymeVaultBalanceBefore:', + enzymeVaultBalanceBefore.toString() + ); + + console.log('sweeping deposits...'); const txSweep = await pm.connect(admin).sweepDeposit(); - await txSweep.wait(); + const txReceipt = await txSweep.wait(); + + // console.log('txEvents:', txReceipt.events); console.log( 'pmSweptBalanceForUUIDAfter:', - await pm.getSweptAmountForUUID(uuid) + (await pm.getSweptAmountForUUID(uuid)).toString() ); const pmBalanceAfterSweep = await dlcBTC.balanceOf(pm.address); console.log('pmBalanceAfterSweep:', pmBalanceAfterSweep.toString()); - const enzymeBalance = await dlcBTC.balanceOf( - '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee' + const enzymeVaultBalanceAfter = await dlcBTC.balanceOf(enzymeVaultAddress); + console.log('enzymeVaultBalanceAfter:', enzymeVaultBalanceAfter.toString()); + + console.log('withdrawing funds...'); + const withdrawAmount = 50000; + const txWithdraw = await pm + .connect(admin) + .withdraw(uuid, withdrawAmount, fakeTapPK, fakeWdTxId); + const txReceiptWithdraw = await txWithdraw.wait(); + + const pmBalanceAfterWithdraw = await dlcBTC.balanceOf(pm.address); + console.log('pmBalanceAfterWithdraw:', pmBalanceAfterWithdraw.toString()); + const enzymeVaultBalanceAfterWithdraw = + await dlcBTC.balanceOf(enzymeVaultAddress); + console.log( + 'enzymeVaultBalanceAfterWithdraw:', + enzymeVaultBalanceAfterWithdraw.toString() + ); + const sweptAmountsAfterWithdraw = await pm.getSweptAmountForUUID(uuid); + console.log( + 'sweptAmountsAfterWithdraw:', + sweptAmountsAfterWithdraw.toString() ); - console.log('enzymeBalance:', enzymeBalance.toString()); + + // withdraw funds + // set pending + // set funded with less amount + // sweepredeem + // check balances + + // const signatureBytesForPending = await getSignatures( + // { + // uuid, + // btcTxId: fakeWdTxId, + // functionString: 'set-status-pending', + // newLockedAmount: 0, + // }, + // attestors, + // 3 + // ); + // const tx3 = await dlcManager + // .connect(attestor1) + // .setStatusPending( + // uuid, + // fakeWdTxId, + // signatureBytesForPending, + // fakeTapPK, + // 0 + // ); + // await tx3.wait(); + // const signatureBytesForFunding2 = await getSignatures( + // { + // uuid, + // btcTxId: fakeWdTxId, + // functionString: 'set-status-funded', + // newLockedAmount: newAmountLocked, + // }, + // attestors, + // 3 + // ); + // const tx4 = await dlcManager + // .connect(attestor1) + // .setStatusFunded( + // uuid, + // fakeWdTxId, + // signatureBytesForFunding2, + // newAmountLocked + // ); + // await tx4.wait(); } main() From eab9bbc969b47dfaf4c5cbf8ff691e7ac2ae552c Mon Sep 17 00:00:00 2001 From: scolear Date: Mon, 28 Oct 2024 19:27:38 +0100 Subject: [PATCH 04/27] wip/feat: PoolMerchant with integration allocations --- contracts/PoolMerchant.sol | 658 +++++++++++++++++++++++------- contracts/integrations/enzyme.sol | 214 ++++++++++ 2 files changed, 721 insertions(+), 151 deletions(-) create mode 100644 contracts/integrations/enzyme.sol diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 56f37d0..b11462a 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -8,207 +8,563 @@ pragma solidity 0.8.18; import "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "./DLCLinkLibrary.sol"; -import "./DLCManager.sol"; - -// interface IEnzymeVault { -// function buyShares( -// uint256 _investmentAmount, -// uint256 _minSharesQuantity -// ) external returns (uint256); -// } - -interface IGlobalConfigLibComptrollerV4 { - function buyShares( - uint256 _investmentAmount, - uint256 _minSharesQuantity - ) external returns (uint256 sharesReceived_); - - function getDenominationAsset() - external - view - returns (address denominationAsset_); - - function redeemSharesForSpecificAssets( - address _recipient, - uint256 _sharesQuantity, - address[] calldata _payoutAssets, - uint256[] calldata _payoutAssetPercentages - ) external returns (uint256[] memory payoutAmounts_); - - function redeemSharesInKind( - address _recipient, - uint256 _sharesQuantity, - address[] calldata _additionalAssets, - address[] calldata _assetsToSkip - ) - external - returns ( - address[] memory payoutAssets_, - uint256[] memory payoutAmounts_ - ); + +interface IDLCManager { + function setupPendingVault( + bytes32 _uuid, + string calldata _taprootPubKey, + string calldata _wdTxId + ) external; + + function withdraw(bytes32 uuid, uint256 amount) external; + function getDLC(bytes32 uuid) external view returns (DLCLink.DLC memory); +} + +interface IIntegration { + function deposit(uint256 amount) external returns (uint256 shares); + function withdraw(uint256 shares) external returns (uint256 amount); } contract PoolMerchant is Initializable, + ERC165Upgradeable, AccessControlDefaultAdminRulesUpgradeable, - PausableUpgradeable + PausableUpgradeable, + ReentrancyGuardUpgradeable, + IERC721Receiver, + IERC1155Receiver { using DLCLink for DLCLink.DLC; - using Strings for string; - using SafeERC20 for IERC20; + using DLCLink for DLCLink.DLCStatus; + + //////////////////////////////////////////////////////////////// + // STATE VARIABLES // + //////////////////////////////////////////////////////////////// - bytes32 public constant DLC_ADMIN_ROLE = - 0x2bf88000669ee6f7a648a231f4adbc117f5a8e34f980c08420b9b9a9f2640aa1; // keccak256("DLC_ADMIN_ROLE") + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + bytes32 public constant HARVESTER_ROLE = keccak256("HARVESTER_ROLE"); - IGlobalConfigLibComptrollerV4 public enzymeVault; - DLCManager public dlcManager; + IDLCManager public dlcManager; IERC20 public dlcBTC; - uint256 private _nonce; - mapping(bytes32 => string) public uuidToTaprootPubkey; - mapping(bytes32 => address) public uuidToUserAddress; - mapping(bytes32 => uint256) public sweptAmounts; - function initialize( - address defaultAdmin, - address dlcAdminRole, - address dlcManagerContract, - address dlcBTCContract, - address enzymeVaultContract - ) public initializer { - __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); - _grantRole(DLC_ADMIN_ROLE, dlcAdminRole); - dlcManager = DLCManager(dlcManagerContract); - dlcBTC = IERC20(dlcBTCContract); - enzymeVault = IGlobalConfigLibComptrollerV4(enzymeVaultContract); - _nonce = 0; + struct RewardToken { + address tokenAddress; + bool isActive; + } + + struct UserReward { + uint256 lastClaimedAt; + uint256 pendingAmount; // For ERC20s + } + + struct VaultInfo { + mapping(address => uint256) integrationShares; // integration -> shares + mapping(address => mapping(address => UserReward)) rewards; // integration -> token -> reward + uint256 totalAllocated; // Track total amount allocated to integrations } + struct Integration { + IIntegration strategy; + bool isActive; + uint256 totalShares; + address[] supportedRewardTokens; + } + + mapping(bytes32 => VaultInfo) internal _vaults; + mapping(address => Integration) public integrations; + mapping(address => RewardToken) public rewardTokens; + address[] public activeIntegrations; + mapping(address => bytes32[]) private _integrationVaults; // integration -> array of vault IDs + mapping(address => mapping(bytes32 => uint256)) private _vaultIndices; // integration -> vault -> index in array + + uint256 public totalValueLocked; + uint256[50] private __gap; + + //////////////////////////////////////////////////////////////// + // EVENTS // + //////////////////////////////////////////////////////////////// + + event IntegrationAdded( + address indexed integration, + address[] supportedRewards + ); + event RewardTokenAdded(address indexed token); + event SharesAllocated( + bytes32 indexed vaultId, + address indexed integration, + uint256 shares + ); + event RewardsHarvested( + address indexed integration, + address indexed rewardToken, + address indexed harvester, + uint256 amount + ); + event RewardsClaimed( + bytes32 indexed vaultId, + address indexed integration, + address indexed rewardToken, + uint256 amount + ); + event VaultWithdrawn(bytes32 indexed vaultId, uint256 amount); + + //////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + //////////////////////////////////////////////////////////////// + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } - error VaultAlreadyExists(bytes32 uuid); + function initialize( + address _dlcManager, + address _dlcBTC, + address defaultAdmin + ) public initializer { + __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); + __Pausable_init(); + __ReentrancyGuard_init(); + __ERC165_init(); - function getNewUUID(address userAddress) public view returns (bytes32) { - return dlcManager.generateUUID(userAddress, block.timestamp); + dlcManager = IDLCManager(_dlcManager); + dlcBTC = IERC20(_dlcBTC); + + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } - // TODO: auth - // called by attestors + //////////////////////////////////////////////////////////////// + // MAIN FUNCTIONS // + //////////////////////////////////////////////////////////////// + function createPendingVault( - address userAddress, - bytes32 uuid, - string memory taprootPubkey, - string calldata wdTxId - ) public { - if (uuidToTaprootPubkey[uuid].equal(taprootPubkey)) { - revert VaultAlreadyExists(uuid); + bytes32 vaultId, + string calldata taprootPubKey, + string calldata withdrawalTxId + ) external onlyRole(OPERATOR_ROLE) whenNotPaused { + // Check if vault exists by querying DLCManager + DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + require(dlc.uuid == bytes32(0), "Vault exists"); + + // Create pending vault in DLCManager + dlcManager.setupPendingVault(vaultId, taprootPubKey, withdrawalTxId); + + // Initialize our tracking (no need to store DLC data) + _vaults[vaultId].integrationShares[address(0)] = 0; // Just initialize the mapping + } + + function withdrawFromVault( + bytes32 vaultId, + uint256 amount + ) external nonReentrant whenNotPaused { + DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + require(dlc.uuid != bytes32(0), "Vault does not exist"); + + // First withdraw from any active integrations + _withdrawFromIntegrations(dlc.uuid, amount); + + // Then withdraw from DLCManager + dlcManager.withdraw(dlc.uuid, amount); + + emit VaultWithdrawn(dlc.uuid, amount); + } + + // Harvesting rewards explained: + // 1. External protocols (like Enzyme, YieldNest) generate rewards + // 2. A harvester (automated or manual) collects these rewards + // 3. The harvester calls this function to distribute rewards to vault holders + function harvestRewards( + address integration, + address rewardToken, + uint256 amount + ) external onlyRole(HARVESTER_ROLE) nonReentrant whenNotPaused { + require(integrations[integration].isActive, "Integration not active"); + require(rewardTokens[rewardToken].isActive, "Invalid reward token"); + + Integration storage integ = integrations[integration]; + uint256 totalShares = integ.totalShares; + + // Get all vaults with shares in this integration + bytes32[] memory activeVaults = _getActiveVaultsForIntegration( + integration + ); + + // Distribute ERC20 rewards proportionally + for (uint256 i = 0; i < activeVaults.length; i++) { + bytes32 vaultId = activeVaults[i]; + uint256 vaultShares = _vaults[vaultId].integrationShares[ + integration + ]; + + if (vaultShares > 0) { + UserReward storage reward = _vaults[vaultId].rewards[ + integration + ][rewardToken]; + uint256 vaultReward = (amount * vaultShares) / totalShares; + reward.pendingAmount += vaultReward; + reward.lastClaimedAt = block.timestamp; + } } - uuidToTaprootPubkey[uuid] = taprootPubkey; - uuidToUserAddress[uuid] = userAddress; - dlcManager.setupPendingVault(uuid, taprootPubkey, wdTxId); + + emit RewardsHarvested(integration, rewardToken, msg.sender, amount); } - function getSharesForUUID(bytes32 uuid) public view returns (uint256) { - DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); - uint256 shares = _dlc.valueMinted; - return shares; + // Claim rewards (ERC20s only) + // TODO: add auth + function claimRewards( + bytes32 vaultId, + address integration, + address rewardToken + ) external nonReentrant whenNotPaused { + UserReward storage reward = _vaults[vaultId].rewards[integration][ + rewardToken + ]; + require(reward.pendingAmount > 0, "No rewards to claim"); + + uint256 amount = reward.pendingAmount; + reward.pendingAmount = 0; + + require( + IERC20(rewardToken).transfer(msg.sender, amount), + "Reward transfer failed" + ); + + emit RewardsClaimed(vaultId, integration, rewardToken, amount); } - function getSweptAmountForUUID(bytes32 uuid) public view returns (uint256) { - return sweptAmounts[uuid]; + //////////////////////////////////////////////////////////////// + // INTEGRATIONS // + //////////////////////////////////////////////////////////////// + + function setIntegration( + address integration, + address[] calldata supportedRewards + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + // Validate reward tokens + for (uint256 i = 0; i < supportedRewards.length; i++) { + require( + rewardTokens[supportedRewards[i]].isActive, + "Invalid reward token" + ); + } + + integrations[integration] = Integration({ + strategy: IIntegration(integration), + isActive: true, + totalShares: integrations[integration].totalShares, // Preserve existing shares if any + supportedRewardTokens: supportedRewards + }); + + // Update active integrations list if new + if (!_isInActiveIntegrations(integration)) { + activeIntegrations.push(integration); + } + + emit IntegrationAdded(integration, supportedRewards); } - // called by attestors - function sweepDeposit() public { - DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( - address(this) + // Allocate dlcBTC to an integration + function allocateToIntegration( + bytes32 vaultId, + address integration + ) external onlyRole(OPERATOR_ROLE) nonReentrant whenNotPaused { + require(integrations[integration].isActive, "Integration not active"); + + // Get current vault state + DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + require(dlc.valueMinted > 0, "Vault not funded"); + + // Calculate amount available to allocate + uint256 unallocated = dlc.valueMinted - _vaults[vaultId].totalAllocated; + require(unallocated > 0, "Nothing to allocate"); + + // Approve and deposit to integration + require( + dlcBTC.approve(address(integration), unallocated), + "Approval failed" + ); + uint256 shares = integrations[integration].strategy.deposit( + unallocated ); - for (uint256 i = 0; i < _allDLCs.length; i++) { - bytes32 uuid = _allDLCs[i].uuid; - uint256 currentValueMinted = _allDLCs[i].valueMinted; - uint256 sweptAmount = sweptAmounts[uuid]; + // Update share accounting + _vaults[vaultId].integrationShares[integration] += shares; + _vaults[vaultId].totalAllocated += unallocated; + integrations[integration].totalShares += shares; - if (currentValueMinted > sweptAmount) { - uint256 difference = currentValueMinted - sweptAmount; + _addVaultToIntegration(integration, vaultId); - // Approve the Enzyme vault to spend dlcBTC tokens - dlcBTC.approve(address(enzymeVault), difference); + emit SharesAllocated(vaultId, integration, shares); + } - enzymeVault.buyShares(difference, 1); + function _isInActiveIntegrations( + address integration + ) internal view returns (bool) { + for (uint256 i = 0; i < activeIntegrations.length; i++) { + if (activeIntegrations[i] == integration) { + return true; + } + } + return false; + } - sweptAmounts[uuid] = currentValueMinted; // Update the local tracker + function _withdrawFromIntegrations( + bytes32 vaultId, + uint256 totalAmount + ) internal { + uint256 remainingAmount = totalAmount; + + for ( + uint256 i = 0; + i < activeIntegrations.length && remainingAmount > 0; + i++ + ) { + address integration = activeIntegrations[i]; + uint256 shareAmount = _vaults[vaultId].integrationShares[ + integration + ]; + + if (shareAmount > 0) { + Integration storage integ = integrations[integration]; + uint256 withdrawAmount = (shareAmount * totalAmount) / + integ.totalShares; + + if (withdrawAmount > 0) { + uint256 received = integ.strategy.withdraw(withdrawAmount); + remainingAmount -= received; + _vaults[vaultId].integrationShares[ + integration + ] -= withdrawAmount; + _vaults[vaultId].totalAllocated -= received; // Update allocated tracking + integ.totalShares -= withdrawAmount; + + if (_vaults[vaultId].integrationShares[integration] == 0) { + _removeVaultFromIntegration(integration, vaultId); + } + } } } + + require(remainingAmount == 0, "Insufficient liquidity in integrations"); } - // attestors call this - function withdraw( - bytes32 uuid, - uint256 amount, - string memory taprootPubkey, - string calldata /*wdTxId*/ - ) public { - require( - keccak256(abi.encodePacked(uuidToTaprootPubkey[uuid])) == - keccak256(abi.encodePacked(taprootPubkey)), - "Invalid taproot pubkey" - ); + // Helper function to get active _vaults for an integration + function _getActiveVaultsForIntegration( + address integration + ) internal view returns (bytes32[] memory) { + bytes32[] memory allVaults = _integrationVaults[integration]; + uint256 activeCount = 0; + + // First count active vaults + for (uint256 i = 0; i < allVaults.length; i++) { + if (_vaults[allVaults[i]].integrationShares[integration] > 0) { + activeCount++; + } + } - address[] memory payoutAssets = new address[](1); - payoutAssets[0] = address(dlcBTC); - uint256[] memory payoutPercentage = new uint256[](1); - payoutPercentage[0] = 10000; + // Create result array with exact size + bytes32[] memory activeVaults = new bytes32[](activeCount); + uint256 currentIndex = 0; + + // Fill result array + for ( + uint256 i = 0; + i < allVaults.length && currentIndex < activeCount; + i++ + ) { + if (_vaults[allVaults[i]].integrationShares[integration] > 0) { + activeVaults[currentIndex] = allVaults[i]; + currentIndex++; + } + } - enzymeVault.redeemSharesForSpecificAssets( - address(this), - amount, - payoutAssets, - payoutPercentage - ); + return activeVaults; + } + + //////////////////////////////////////////////////////////////// + // VAULT FUNCTIONS // + //////////////////////////////////////////////////////////////// + + function getVaultShares( + bytes32 vaultId, + address integration + ) external view returns (uint256) { + return _vaults[vaultId].integrationShares[integration]; + } + + function getVaultReward( + bytes32 vaultId, + address integration, + address rewardToken + ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { + UserReward storage reward = _vaults[vaultId].rewards[integration][ + rewardToken + ]; + return (reward.lastClaimedAt, reward.pendingAmount); + } + + // Helper function to get total shares in an integration for a vault + function getVaultTotalShares( + bytes32 vaultId + ) external view returns (uint256 totalShares) { + for (uint256 i = 0; i < activeIntegrations.length; i++) { + totalShares += _vaults[vaultId].integrationShares[ + activeIntegrations[i] + ]; + } + } + + function getUnallocatedAmount( + bytes32 vaultId + ) public view returns (uint256) { + DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + return dlc.valueMinted - _vaults[vaultId].totalAllocated; + } - dlcManager.withdraw(uuid, amount); - DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); - sweptAmounts[uuid] = _dlc.valueMinted; // Update the local tracker + function getVaultAllocationDetails( + bytes32 vaultId + ) + external + view + returns ( + uint256 totalMinted, + uint256 totalAllocated, + uint256 unallocated + ) + { + DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + totalMinted = dlc.valueMinted; + totalAllocated = _vaults[vaultId].totalAllocated; + unallocated = totalMinted - totalAllocated; } - // function sweepRedeem() public { - // DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( - // address(this) - // ); + function _addVaultToIntegration( + address integration, + bytes32 vaultId + ) internal { + if (_vaultIndices[integration][vaultId] == 0) { + // 0 means not found + _integrationVaults[integration].push(vaultId); + _vaultIndices[integration][vaultId] = _integrationVaults[ + integration + ].length; + } + } - // for (uint256 i = 0; i < _allDLCs.length; i++) { - // bytes32 uuid = _allDLCs[i].uuid; - // uint256 currentValueMinted = _allDLCs[i].valueMinted; - // uint256 sweptAmount = sweptAmounts[uuid]; + // Add this helper function + function _removeVaultFromIntegration( + address integration, + bytes32 vaultId + ) internal { + uint256 index = _vaultIndices[integration][vaultId]; + if (index > 0) { + // If vault exists in array + index--; // Convert from 1-based to 0-based index + + // Get the last element + uint256 lastIndex = _integrationVaults[integration].length - 1; + if (index != lastIndex) { + // Move last element to the removed element's position + bytes32 lastVault = _integrationVaults[integration][lastIndex]; + _integrationVaults[integration][index] = lastVault; + _vaultIndices[integration][lastVault] = index + 1; // Update to 1-based index + } - // if (currentValueMinted < sweptAmount) { - // uint256 difference = sweptAmount - currentValueMinted; + // Remove last element + _integrationVaults[integration].pop(); + delete _vaultIndices[integration][vaultId]; + } + } + + //////////////////////////////////////////////////////////////// + // ADMIN FUNCTIONS // + //////////////////////////////////////////////////////////////// - // // Approve the Enzyme vault to spend dlcBTC tokens - // // dlcBTC.approve(address(enzymeVault), difference); + // Reward token management + function addRewardToken( + address token + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(!rewardTokens[token].isActive, "Token already added"); - // address[] memory payoutAssets = new address[](1); - // payoutAssets[0] = address(dlcBTC); - // uint256[] memory payoutPercentage = new uint256[](1); - // payoutPercentage[0] = 100; + // Verify it's an ERC20 + IERC20(token).totalSupply(); // Will revert if not ERC20 - // enzymeVault.redeemSharesForSpecificAssets( - // address(this), - // difference, - // payoutAssets, - // payoutPercentage - // ); + rewardTokens[token] = RewardToken({ + tokenAddress: token, + isActive: true + }); - // sweptAmounts[uuid] = currentValueMinted; // Update the local tracker - // } - // } - // } + emit RewardTokenAdded(token); + } + + function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _pause(); + } + + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } + + //////////////////////////////////////////////////////////////// + // UTILITIES // + //////////////////////////////////////////////////////////////// + + // Required interface implementations + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface( + bytes4 interfaceId + ) + public + view + virtual + override( + ERC165Upgradeable, + AccessControlDefaultAdminRulesUpgradeable, + IERC165 + ) + returns (bool) + { + return + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + super.supportsInterface(interfaceId); + } } diff --git a/contracts/integrations/enzyme.sol b/contracts/integrations/enzyme.sol new file mode 100644 index 0000000..e63975e --- /dev/null +++ b/contracts/integrations/enzyme.sol @@ -0,0 +1,214 @@ +// // SPDX-License-Identifier: MIT +// // ___ __ ___ __ _ _ +// // / \/ / / __\ / /(_)_ __ | | __ +// // / /\ / / / / / / | | '_ \| |/ / +// // / /_// /__/ /____/ /__| | | | | < +// // /___,'\____|____(_)____/_|_| |_|_|\_\ + +// pragma solidity 0.8.18; + +// import "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol"; +// import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +// import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +// import "@openzeppelin/contracts/utils/Strings.sol"; +// import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// import "./DLCLinkLibrary.sol"; +// import "./DLCManager.sol"; + +// // interface IEnzymeVault { +// // function buyShares( +// // uint256 _investmentAmount, +// // uint256 _minSharesQuantity +// // ) external returns (uint256); +// // } + +// interface IGlobalConfigLibComptrollerV4 { +// function buyShares( +// uint256 _investmentAmount, +// uint256 _minSharesQuantity +// ) external returns (uint256 sharesReceived_); + +// function getDenominationAsset() +// external +// view +// returns (address denominationAsset_); + +// function redeemSharesForSpecificAssets( +// address _recipient, +// uint256 _sharesQuantity, +// address[] calldata _payoutAssets, +// uint256[] calldata _payoutAssetPercentages +// ) external returns (uint256[] memory payoutAmounts_); + +// function redeemSharesInKind( +// address _recipient, +// uint256 _sharesQuantity, +// address[] calldata _additionalAssets, +// address[] calldata _assetsToSkip +// ) +// external +// returns ( +// address[] memory payoutAssets_, +// uint256[] memory payoutAmounts_ +// ); +// } + +// contract PoolMerchant is +// Initializable, +// AccessControlDefaultAdminRulesUpgradeable, +// PausableUpgradeable +// { +// using DLCLink for DLCLink.DLC; +// using Strings for string; +// using SafeERC20 for IERC20; + +// bytes32 public constant DLC_ADMIN_ROLE = +// 0x2bf88000669ee6f7a648a231f4adbc117f5a8e34f980c08420b9b9a9f2640aa1; // keccak256("DLC_ADMIN_ROLE") + +// IGlobalConfigLibComptrollerV4 public enzymeVault; +// DLCManager public dlcManager; +// IERC20 public dlcBTC; +// uint256 private _nonce; +// mapping(bytes32 => string) public uuidToTaprootPubkey; +// mapping(bytes32 => address) public uuidToUserAddress; +// mapping(bytes32 => uint256) public sweptAmounts; + +// function initialize( +// address defaultAdmin, +// address dlcAdminRole, +// address dlcManagerContract, +// address dlcBTCContract, +// address enzymeVaultContract +// ) public initializer { +// __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); +// _grantRole(DLC_ADMIN_ROLE, dlcAdminRole); +// dlcManager = DLCManager(dlcManagerContract); +// dlcBTC = IERC20(dlcBTCContract); +// enzymeVault = IGlobalConfigLibComptrollerV4(enzymeVaultContract); +// _nonce = 0; +// } + +// /// @custom:oz-upgrades-unsafe-allow constructor +// constructor() { +// _disableInitializers(); +// } + +// error VaultAlreadyExists(bytes32 uuid); + +// function getNewUUID(address userAddress) public view returns (bytes32) { +// return dlcManager.generateUUID(userAddress, block.timestamp); +// } + +// // TODO: auth +// // called by attestors +// function createPendingVault( +// address userAddress, +// bytes32 uuid, +// string memory taprootPubkey, +// string calldata wdTxId +// ) public { +// if (uuidToTaprootPubkey[uuid].equal(taprootPubkey)) { +// revert VaultAlreadyExists(uuid); +// } +// uuidToTaprootPubkey[uuid] = taprootPubkey; +// uuidToUserAddress[uuid] = userAddress; +// dlcManager.setupPendingVault(uuid, taprootPubkey, wdTxId); +// } + +// function getSharesForUUID(bytes32 uuid) public view returns (uint256) { +// DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); +// uint256 shares = _dlc.valueMinted; +// return shares; +// } + +// function getSweptAmountForUUID(bytes32 uuid) public view returns (uint256) { +// return sweptAmounts[uuid]; +// } + +// // called by attestors +// function sweepDeposit() public { +// DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( +// address(this) +// ); + +// for (uint256 i = 0; i < _allDLCs.length; i++) { +// bytes32 uuid = _allDLCs[i].uuid; +// uint256 currentValueMinted = _allDLCs[i].valueMinted; +// uint256 sweptAmount = sweptAmounts[uuid]; + +// if (currentValueMinted > sweptAmount) { +// uint256 difference = currentValueMinted - sweptAmount; + +// // Approve the Enzyme vault to spend dlcBTC tokens +// dlcBTC.approve(address(enzymeVault), difference); + +// enzymeVault.buyShares(difference, 1); + +// sweptAmounts[uuid] = currentValueMinted; // Update the local tracker +// } +// } +// } + +// // attestors call this +// function withdraw( +// bytes32 uuid, +// uint256 amount, +// string memory taprootPubkey, +// string calldata /*wdTxId*/ +// ) public { +// require( +// keccak256(abi.encodePacked(uuidToTaprootPubkey[uuid])) == +// keccak256(abi.encodePacked(taprootPubkey)), +// "Invalid taproot pubkey" +// ); + +// address[] memory payoutAssets = new address[](1); +// payoutAssets[0] = address(dlcBTC); +// uint256[] memory payoutPercentage = new uint256[](1); +// payoutPercentage[0] = 10000; + +// enzymeVault.redeemSharesForSpecificAssets( +// address(this), +// amount, +// payoutAssets, +// payoutPercentage +// ); + +// dlcManager.withdraw(uuid, amount); +// DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); +// sweptAmounts[uuid] = _dlc.valueMinted; // Update the local tracker +// } + +// // function sweepRedeem() public { +// // DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( +// // address(this) +// // ); + +// // for (uint256 i = 0; i < _allDLCs.length; i++) { +// // bytes32 uuid = _allDLCs[i].uuid; +// // uint256 currentValueMinted = _allDLCs[i].valueMinted; +// // uint256 sweptAmount = sweptAmounts[uuid]; + +// // if (currentValueMinted < sweptAmount) { +// // uint256 difference = sweptAmount - currentValueMinted; + +// // // Approve the Enzyme vault to spend dlcBTC tokens +// // // dlcBTC.approve(address(enzymeVault), difference); + +// // address[] memory payoutAssets = new address[](1); +// // payoutAssets[0] = address(dlcBTC); +// // uint256[] memory payoutPercentage = new uint256[](1); +// // payoutPercentage[0] = 100; + +// // enzymeVault.redeemSharesForSpecificAssets( +// // address(this), +// // difference, +// // payoutAssets, +// // payoutPercentage +// // ); + +// // sweptAmounts[uuid] = currentValueMinted; // Update the local tracker +// // } +// // } +// // } +// } From 84cafb87065cbfccb40d83516094c5f02831c051 Mon Sep 17 00:00:00 2001 From: scolear Date: Mon, 28 Oct 2024 19:59:52 +0100 Subject: [PATCH 05/27] chore: comment out outdated enzyme script --- scripts/enzyme-tests.js | 586 ++++++++++++++++++++-------------------- 1 file changed, 293 insertions(+), 293 deletions(-) diff --git a/scripts/enzyme-tests.js b/scripts/enzyme-tests.js index 8d1b424..97137eb 100644 --- a/scripts/enzyme-tests.js +++ b/scripts/enzyme-tests.js @@ -1,293 +1,293 @@ -const hardhat = require('hardhat'); -const { - loadDeploymentInfo, -} = require('./helpers/deployment-handlers_versioned'); -const callManagerContractFunction = require('./helpers/00-call-dlc-manager-fn'); - -const { getSignatures, setSigners } = require('../test/utils'); - -// This script requires forking Arbitrum Mainnet from at least block 260000776 -async function main() { - await hardhat.run('compile'); - const accounts = await hardhat.ethers.getSigners(); - let attestor1, attestor2, attestor3; - let attestors; - attestor1 = accounts[6]; - attestor2 = accounts[7]; - attestor3 = accounts[8]; - attestors = [attestor1, attestor2, attestor3]; - let btcFeeRecipient = '0x000001'; - - const dlcAdminSafe = await hardhat.ethers.getImpersonatedSigner( - '0xaA2949C5285C2f2887ABD567865344240c29d619' - ); - const coordinator = await hardhat.ethers.getImpersonatedSigner( - '0x3355977947F84C2b1CAE7D2903a72958aEE185e2' - ); - const enzymeManager = await hardhat.ethers.getImpersonatedSigner( - '0x0DD4f29E21F10cb2E485cf9bDAb9F2dD1f240Bfa' - ); - - const enzymeComptrollerAddress = - '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee'; - const enzymeVaultAddress = '0xe1f20e9855b2bce29f92579ba193230389795b46'; - const admin = accounts[0]; - const userAddress = accounts[1].address; - - const deployInfoManager = await loadDeploymentInfo( - hardhat.network.name, - 'DLCManager' - ); - const dlcManagerExisting = new hardhat.ethers.Contract( - '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', - deployInfoManager.contract.abi, - admin - ); - - const deployInfoToken = await loadDeploymentInfo( - hardhat.network.name, - 'DLCBTC' - ); - - const dlcBTC = new hardhat.ethers.Contract( - '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', - deployInfoToken.contract.abi, - admin - ); - let dlcManager; - - const dlcManagerExists = await hardhat.ethers.getContractAt( - 'DLCManager', - '0xe4097Ee7b1AE8a7E5DdCaD820Ca31ee4d2d9Ef92', - admin - ); - - // console.log('dlcManagerExists:', dlcManagerExists); - - // TODO: this check doesnt really work - if (process.env.FORCE_DEPLOY) { - const DLCManager = - await hardhat.ethers.getContractFactory('DLCManager'); - dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - 2, - dlcBTC.address, - btcFeeRecipient, - ]); - await dlcManager.deployed(); - await setSigners(dlcManager, attestors); - - const txTranferOwnership = await dlcManagerExisting - .connect(dlcAdminSafe) - .transferTokenContractOwnership(dlcManager.address); - await txTranferOwnership.wait(); - } else { - dlcManager = dlcManagerExists; - } - - console.log('dlcManager:', dlcManager.address); - console.log('dlcBTC:', dlcBTC.address); - console.log('dlcBTC owner:', await dlcBTC.owner()); - - let poolMerchant; - - const pmExists = await hardhat.ethers.getContractAt( - 'PoolMerchant', - '0xf96C190E181b38c840B7832BbA9E8D527250a5FB', - admin - ); - - // console.log('pmExists:', pmExists); - - if (process.env.FORCE_DEPLOY) { - const PoolMerchant = - await hardhat.ethers.getContractFactory('PoolMerchant'); - poolMerchant = await hardhat.upgrades.deployProxy(PoolMerchant, [ - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', - dlcManager.address, - dlcBTC.address, - enzymeComptrollerAddress, - ]); - await poolMerchant.deployed(); - } else { - poolMerchant = pmExists; - } - - const pm = new hardhat.ethers.Contract( - poolMerchant.address, - // '0x8d6e07a6c15e1cdb881d665e0da4d7c2004a1929', - // ['function getNewUUID(address) view returns (bytes32)'], - poolMerchant.interface, - admin - ); - console.log('pm:', pm.address); - const uuid = await pm.getNewUUID(admin.address); - console.log('uuid:', uuid); - - const enzymeVault = new hardhat.ethers.Contract( - enzymeComptrollerAddress, - [ - 'function buyShares(uint256, uint256) external returns (uint256)', - 'function getDenominationAsset() external view returns (address)', - 'function vaultCallOnContract(address,bytes4,bytes) external', - ], - admin - ); - const asset = await enzymeVault.getDenominationAsset(); - console.log('asset on enzyme:', asset); - - await enzymeVault - .connect(enzymeManager) - .vaultCallOnContract( - '0x2C6bef68DAbf0494bB5F727E63c8FB54f7D2c287', - '0x8da3d736', - '0x000000000000000000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f96c190e181b38c840b7832bba9e8d527250a5fb' - ); - - // set up pending vault - // set it to funded - // PM should have the funds - // call sweep - // enzyme shoud have the funds - - const fakeWdTxId = 'fakeWdTxId'; - const fakeTapPK = 'fakeTapPK'; - const deposit = 100000000; - - try { - await dlcManager.connect(admin).whitelistAddress(pm.address); - console.log('whitelisted pm on dlcManager'); - } catch (error) { - console.error('error whitelisting pm on dlcManager:', error); - } - await pm - .connect(admin) - .createPendingVault(userAddress, uuid, fakeTapPK, fakeWdTxId); - console.log('created pending vault through pm'); - const dlc = await dlcManager.getDLC(uuid); - // console.log('dlc:', dlc); - const pmBalanceBefore = await dlcBTC.balanceOf(pm.address); - console.log('pmBalanceBefore Funded:', pmBalanceBefore.toString()); - - const signatureBytesForFunding = await getSignatures( - { - uuid, - btcTxId: fakeWdTxId, - functionString: 'set-status-funded', - newLockedAmount: deposit, - }, - attestors, - 3 - ); - const tx2 = await dlcManager - .connect(attestor1) - .setStatusFunded(uuid, fakeWdTxId, signatureBytesForFunding, deposit); - await tx2.wait(); - - const pmBalanceAfter = await dlcBTC.balanceOf(pm.address); - console.log('pmBalanceAfter Funded:', pmBalanceAfter.toString()); - - console.log( - 'pmSweptBalanceForUUIDBefore:', - (await pm.getSweptAmountForUUID(uuid)).toString() - ); - - const enzymeVaultBalanceBefore = await dlcBTC.balanceOf(enzymeVaultAddress); - console.log( - 'enzymeVaultBalanceBefore:', - enzymeVaultBalanceBefore.toString() - ); - - console.log('sweeping deposits...'); - const txSweep = await pm.connect(admin).sweepDeposit(); - const txReceipt = await txSweep.wait(); - - // console.log('txEvents:', txReceipt.events); - - console.log( - 'pmSweptBalanceForUUIDAfter:', - (await pm.getSweptAmountForUUID(uuid)).toString() - ); - const pmBalanceAfterSweep = await dlcBTC.balanceOf(pm.address); - console.log('pmBalanceAfterSweep:', pmBalanceAfterSweep.toString()); - const enzymeVaultBalanceAfter = await dlcBTC.balanceOf(enzymeVaultAddress); - console.log('enzymeVaultBalanceAfter:', enzymeVaultBalanceAfter.toString()); - - console.log('withdrawing funds...'); - const withdrawAmount = 50000; - const txWithdraw = await pm - .connect(admin) - .withdraw(uuid, withdrawAmount, fakeTapPK, fakeWdTxId); - const txReceiptWithdraw = await txWithdraw.wait(); - - const pmBalanceAfterWithdraw = await dlcBTC.balanceOf(pm.address); - console.log('pmBalanceAfterWithdraw:', pmBalanceAfterWithdraw.toString()); - const enzymeVaultBalanceAfterWithdraw = - await dlcBTC.balanceOf(enzymeVaultAddress); - console.log( - 'enzymeVaultBalanceAfterWithdraw:', - enzymeVaultBalanceAfterWithdraw.toString() - ); - const sweptAmountsAfterWithdraw = await pm.getSweptAmountForUUID(uuid); - console.log( - 'sweptAmountsAfterWithdraw:', - sweptAmountsAfterWithdraw.toString() - ); - - // withdraw funds - // set pending - // set funded with less amount - // sweepredeem - // check balances - - // const signatureBytesForPending = await getSignatures( - // { - // uuid, - // btcTxId: fakeWdTxId, - // functionString: 'set-status-pending', - // newLockedAmount: 0, - // }, - // attestors, - // 3 - // ); - // const tx3 = await dlcManager - // .connect(attestor1) - // .setStatusPending( - // uuid, - // fakeWdTxId, - // signatureBytesForPending, - // fakeTapPK, - // 0 - // ); - // await tx3.wait(); - // const signatureBytesForFunding2 = await getSignatures( - // { - // uuid, - // btcTxId: fakeWdTxId, - // functionString: 'set-status-funded', - // newLockedAmount: newAmountLocked, - // }, - // attestors, - // 3 - // ); - // const tx4 = await dlcManager - // .connect(attestor1) - // .setStatusFunded( - // uuid, - // fakeWdTxId, - // signatureBytesForFunding2, - // newAmountLocked - // ); - // await tx4.wait(); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); - -// +// const hardhat = require('hardhat'); +// const { +// loadDeploymentInfo, +// } = require('./helpers/deployment-handlers_versioned'); +// const callManagerContractFunction = require('./helpers/00-call-dlc-manager-fn'); + +// const { getSignatures, setSigners } = require('../test/utils'); + +// // This script requires forking Arbitrum Mainnet from at least block 260000776 +// async function main() { +// await hardhat.run('compile'); +// const accounts = await hardhat.ethers.getSigners(); +// let attestor1, attestor2, attestor3; +// let attestors; +// attestor1 = accounts[6]; +// attestor2 = accounts[7]; +// attestor3 = accounts[8]; +// attestors = [attestor1, attestor2, attestor3]; +// let btcFeeRecipient = '0x000001'; + +// const dlcAdminSafe = await hardhat.ethers.getImpersonatedSigner( +// '0xaA2949C5285C2f2887ABD567865344240c29d619' +// ); +// const coordinator = await hardhat.ethers.getImpersonatedSigner( +// '0x3355977947F84C2b1CAE7D2903a72958aEE185e2' +// ); +// const enzymeManager = await hardhat.ethers.getImpersonatedSigner( +// '0x0DD4f29E21F10cb2E485cf9bDAb9F2dD1f240Bfa' +// ); + +// const enzymeComptrollerAddress = +// '0x0c164A2708477C8930E8964b3B8EB9038C86Ffee'; +// const enzymeVaultAddress = '0xe1f20e9855b2bce29f92579ba193230389795b46'; +// const admin = accounts[0]; +// const userAddress = accounts[1].address; + +// const deployInfoManager = await loadDeploymentInfo( +// hardhat.network.name, +// 'DLCManager' +// ); +// const dlcManagerExisting = new hardhat.ethers.Contract( +// '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', +// deployInfoManager.contract.abi, +// admin +// ); + +// const deployInfoToken = await loadDeploymentInfo( +// hardhat.network.name, +// 'DLCBTC' +// ); + +// const dlcBTC = new hardhat.ethers.Contract( +// '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', +// deployInfoToken.contract.abi, +// admin +// ); +// let dlcManager; + +// const dlcManagerExists = await hardhat.ethers.getContractAt( +// 'DLCManager', +// '0xe4097Ee7b1AE8a7E5DdCaD820Ca31ee4d2d9Ef92', +// admin +// ); + +// // console.log('dlcManagerExists:', dlcManagerExists); + +// // TODO: this check doesnt really work +// if (process.env.FORCE_DEPLOY) { +// const DLCManager = +// await hardhat.ethers.getContractFactory('DLCManager'); +// dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ +// '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', +// '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', +// 2, +// dlcBTC.address, +// btcFeeRecipient, +// ]); +// await dlcManager.deployed(); +// await setSigners(dlcManager, attestors); + +// const txTranferOwnership = await dlcManagerExisting +// .connect(dlcAdminSafe) +// .transferTokenContractOwnership(dlcManager.address); +// await txTranferOwnership.wait(); +// } else { +// dlcManager = dlcManagerExists; +// } + +// console.log('dlcManager:', dlcManager.address); +// console.log('dlcBTC:', dlcBTC.address); +// console.log('dlcBTC owner:', await dlcBTC.owner()); + +// let poolMerchant; + +// const pmExists = await hardhat.ethers.getContractAt( +// 'PoolMerchant', +// '0xf96C190E181b38c840B7832BbA9E8D527250a5FB', +// admin +// ); + +// // console.log('pmExists:', pmExists); + +// if (process.env.FORCE_DEPLOY) { +// const PoolMerchant = +// await hardhat.ethers.getContractFactory('PoolMerchant'); +// poolMerchant = await hardhat.upgrades.deployProxy(PoolMerchant, [ +// '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', +// '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', +// dlcManager.address, +// dlcBTC.address, +// enzymeComptrollerAddress, +// ]); +// await poolMerchant.deployed(); +// } else { +// poolMerchant = pmExists; +// } + +// const pm = new hardhat.ethers.Contract( +// poolMerchant.address, +// // '0x8d6e07a6c15e1cdb881d665e0da4d7c2004a1929', +// // ['function getNewUUID(address) view returns (bytes32)'], +// poolMerchant.interface, +// admin +// ); +// console.log('pm:', pm.address); +// const uuid = await pm.getNewUUID(admin.address); +// console.log('uuid:', uuid); + +// const enzymeVault = new hardhat.ethers.Contract( +// enzymeComptrollerAddress, +// [ +// 'function buyShares(uint256, uint256) external returns (uint256)', +// 'function getDenominationAsset() external view returns (address)', +// 'function vaultCallOnContract(address,bytes4,bytes) external', +// ], +// admin +// ); +// const asset = await enzymeVault.getDenominationAsset(); +// console.log('asset on enzyme:', asset); + +// await enzymeVault +// .connect(enzymeManager) +// .vaultCallOnContract( +// '0x2C6bef68DAbf0494bB5F727E63c8FB54f7D2c287', +// '0x8da3d736', +// '0x000000000000000000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f96c190e181b38c840b7832bba9e8d527250a5fb' +// ); + +// // set up pending vault +// // set it to funded +// // PM should have the funds +// // call sweep +// // enzyme shoud have the funds + +// const fakeWdTxId = 'fakeWdTxId'; +// const fakeTapPK = 'fakeTapPK'; +// const deposit = 100000000; + +// try { +// await dlcManager.connect(admin).whitelistAddress(pm.address); +// console.log('whitelisted pm on dlcManager'); +// } catch (error) { +// console.error('error whitelisting pm on dlcManager:', error); +// } +// await pm +// .connect(admin) +// .createPendingVault(userAddress, uuid, fakeTapPK, fakeWdTxId); +// console.log('created pending vault through pm'); +// const dlc = await dlcManager.getDLC(uuid); +// // console.log('dlc:', dlc); +// const pmBalanceBefore = await dlcBTC.balanceOf(pm.address); +// console.log('pmBalanceBefore Funded:', pmBalanceBefore.toString()); + +// const signatureBytesForFunding = await getSignatures( +// { +// uuid, +// btcTxId: fakeWdTxId, +// functionString: 'set-status-funded', +// newLockedAmount: deposit, +// }, +// attestors, +// 3 +// ); +// const tx2 = await dlcManager +// .connect(attestor1) +// .setStatusFunded(uuid, fakeWdTxId, signatureBytesForFunding, deposit); +// await tx2.wait(); + +// const pmBalanceAfter = await dlcBTC.balanceOf(pm.address); +// console.log('pmBalanceAfter Funded:', pmBalanceAfter.toString()); + +// console.log( +// 'pmSweptBalanceForUUIDBefore:', +// (await pm.getSweptAmountForUUID(uuid)).toString() +// ); + +// const enzymeVaultBalanceBefore = await dlcBTC.balanceOf(enzymeVaultAddress); +// console.log( +// 'enzymeVaultBalanceBefore:', +// enzymeVaultBalanceBefore.toString() +// ); + +// console.log('sweeping deposits...'); +// const txSweep = await pm.connect(admin).sweepDeposit(); +// const txReceipt = await txSweep.wait(); + +// // console.log('txEvents:', txReceipt.events); + +// console.log( +// 'pmSweptBalanceForUUIDAfter:', +// (await pm.getSweptAmountForUUID(uuid)).toString() +// ); +// const pmBalanceAfterSweep = await dlcBTC.balanceOf(pm.address); +// console.log('pmBalanceAfterSweep:', pmBalanceAfterSweep.toString()); +// const enzymeVaultBalanceAfter = await dlcBTC.balanceOf(enzymeVaultAddress); +// console.log('enzymeVaultBalanceAfter:', enzymeVaultBalanceAfter.toString()); + +// console.log('withdrawing funds...'); +// const withdrawAmount = 50000; +// const txWithdraw = await pm +// .connect(admin) +// .withdraw(uuid, withdrawAmount, fakeTapPK, fakeWdTxId); +// const txReceiptWithdraw = await txWithdraw.wait(); + +// const pmBalanceAfterWithdraw = await dlcBTC.balanceOf(pm.address); +// console.log('pmBalanceAfterWithdraw:', pmBalanceAfterWithdraw.toString()); +// const enzymeVaultBalanceAfterWithdraw = +// await dlcBTC.balanceOf(enzymeVaultAddress); +// console.log( +// 'enzymeVaultBalanceAfterWithdraw:', +// enzymeVaultBalanceAfterWithdraw.toString() +// ); +// const sweptAmountsAfterWithdraw = await pm.getSweptAmountForUUID(uuid); +// console.log( +// 'sweptAmountsAfterWithdraw:', +// sweptAmountsAfterWithdraw.toString() +// ); + +// // withdraw funds +// // set pending +// // set funded with less amount +// // sweepredeem +// // check balances + +// // const signatureBytesForPending = await getSignatures( +// // { +// // uuid, +// // btcTxId: fakeWdTxId, +// // functionString: 'set-status-pending', +// // newLockedAmount: 0, +// // }, +// // attestors, +// // 3 +// // ); +// // const tx3 = await dlcManager +// // .connect(attestor1) +// // .setStatusPending( +// // uuid, +// // fakeWdTxId, +// // signatureBytesForPending, +// // fakeTapPK, +// // 0 +// // ); +// // await tx3.wait(); +// // const signatureBytesForFunding2 = await getSignatures( +// // { +// // uuid, +// // btcTxId: fakeWdTxId, +// // functionString: 'set-status-funded', +// // newLockedAmount: newAmountLocked, +// // }, +// // attestors, +// // 3 +// // ); +// // const tx4 = await dlcManager +// // .connect(attestor1) +// // .setStatusFunded( +// // uuid, +// // fakeWdTxId, +// // signatureBytesForFunding2, +// // newAmountLocked +// // ); +// // await tx4.wait(); +// } + +// main() +// .then(() => process.exit(0)) +// .catch((error) => { +// console.error(error); +// process.exit(1); +// }); + +// // From cb167f48bb925df138f7d1c43255f5681bd4558c Mon Sep 17 00:00:00 2001 From: scolear Date: Tue, 29 Oct 2024 15:12:28 +0100 Subject: [PATCH 06/27] wip: curve integration --- contracts/DLCManager.sol | 3 +++ contracts/PoolMerchant.sol | 4 ---- contracts/integrations/curve.sol | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 contracts/integrations/curve.sol diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index 0343762..357b72e 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -84,6 +84,7 @@ contract DLCManager is error DLCNotPending(); error DLCNotReadyOrFunded(); error DLCNotFunded(); + error DLCAlreadyExists(bytes32 uuid); error ThresholdMinimumReached(uint16 _minimumThreshold); error ThresholdTooLow(uint16 _minimumThreshold); @@ -364,6 +365,8 @@ contract DLCManager is string calldata _taprootPubKey, string calldata _wdTxId ) public onlyWhitelisted whenNotPaused { + if (dlcs[dlcIDsByUUID[_uuid]].uuid == _uuid) + revert DLCAlreadyExists(_uuid); dlcs[_index] = DLCLink.DLC({ uuid: _uuid, protocolContract: msg.sender, // deprecated diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index b11462a..2d54975 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -152,10 +152,6 @@ contract PoolMerchant is string calldata taprootPubKey, string calldata withdrawalTxId ) external onlyRole(OPERATOR_ROLE) whenNotPaused { - // Check if vault exists by querying DLCManager - DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); - require(dlc.uuid == bytes32(0), "Vault exists"); - // Create pending vault in DLCManager dlcManager.setupPendingVault(vaultId, taprootPubKey, withdrawalTxId); diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol new file mode 100644 index 0000000..971e63d --- /dev/null +++ b/contracts/integrations/curve.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// ___ __ ___ __ _ _ +// / \/ / / __\ / /(_)_ __ | | __ +// / /\ / / / / / / | | '_ \| |/ / +// / /_// /__/ /____/ /__| | | | | < +// /___,'\____|____(_)____/_|_| |_|_|\_\ + +pragma solidity 0.8.18; + +interface IIntegration { + function deposit(uint256 amount) external returns (uint256 shares); + function withdraw(uint256 shares) external returns (uint256 amount); +} + +contract CurveIntegration is IIntegration { + // define all that we need to interact with Curve + + function deposit( + uint256 amount + ) external override returns (uint256 shares) { + // Deposit amount into Curve + + // TODO: Implement deposit logic + // deposit amount into Curve Pool through Deposit function + + return shares; + } + + function withdraw( + uint256 shares + ) external override returns (uint256 amount) { + // Withdraw shares from Curve + + // TODO: Implement withdraw logic + // withdraw amount from Curve Pool and return to caller + + return amount; + } +} From 023728422fb75dcf9e503e7cb425365031a22c4b Mon Sep 17 00:00:00 2001 From: Rayerleier <1045643889@qq.com> Date: Wed, 30 Oct 2024 17:26:46 +0800 Subject: [PATCH 07/27] integrations:curve:no-zap --- contracts/integrations/curve.sol | 80 ++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol index 971e63d..22704ec 100644 --- a/contracts/integrations/curve.sol +++ b/contracts/integrations/curve.sol @@ -1,27 +1,69 @@ -// SPDX-License-Identifier: MIT -// ___ __ ___ __ _ _ -// / \/ / / __\ / /(_)_ __ | | __ -// / /\ / / / / / / | | '_ \| |/ / -// / /_// /__/ /____/ /__| | | | | < -// /___,'\____|____(_)____/_|_| |_|_|\_\ - pragma solidity 0.8.18; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + interface IIntegration { function deposit(uint256 amount) external returns (uint256 shares); + function withdraw(uint256 shares) external returns (uint256 amount); } +interface ICurvePool { + function add_liquidity( + uint256[] memory _amounts, + uint256 _min_mint_amount, + address _receiver + ) external returns (uint256); + + function remove_liquidity_one_coin( + uint256 _burn_amount, + int128 i, + uint256 _min_received, + address _receiver + ) external returns (uint256); +} + +interface ICurveGauge { + function deposit( + uint256 _value, + address _user, + bool _claim_rewards + ) external; + + function withdraw(uint256 _value, bool _claim_rewards) external; +} + contract CurveIntegration is IIntegration { - // define all that we need to interact with Curve + address public curvePoolAddress; + address public curveGaugeAddress; + ICurvePool public curvePool; + ICurveGauge public curveGauge; + + constructor(address _curvePoolAddress, address _curveGaugeAddress) { + curvePoolAddress = _curvePoolAddress; + curveGaugeAddress = _curveGaugeAddress; + curvePool = ICurvePool(curvePoolAddress); + curveGauge = ICurveGauge(curveGaugeAddress); + } function deposit( uint256 amount ) external override returns (uint256 shares) { - // Deposit amount into Curve + uint256[] memory amounts; // Create a dynamic array with 2 elements + amounts[0] = 0; // Set the first element to 0 for the first coin + amounts[1] = amount; // Set the second element to `amount` for the second coin - // TODO: Implement deposit logic - // deposit amount into Curve Pool through Deposit function + uint256 minMintAmount = 0; // Set the acceptable minimum for LP tokens + address receiver = address(this); // The contract itself will receive the LP tokens + + // Step 1: Add liquidity to the Curve pool + shares = curvePool.add_liquidity(amounts, minMintAmount, receiver); + + // Step 2: Approve the Gauge to spend LP tokens + IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); + + // Step 3: Deposit LP tokens into Curve Gauge for rewards + curveGauge.deposit(shares, msg.sender, false); return shares; } @@ -29,10 +71,20 @@ contract CurveIntegration is IIntegration { function withdraw( uint256 shares ) external override returns (uint256 amount) { - // Withdraw shares from Curve + // Step 1: Withdraw LP tokens from the Gauge + curveGauge.withdraw(shares, false); + + // Step 2: Withdraw the second coin from Curve pool + int128 coinIndex = 1; // Set to 1 for the second coin + uint256 minReceived = 0; // Minimum amount of coin to receive + address receiver = msg.sender; - // TODO: Implement withdraw logic - // withdraw amount from Curve Pool and return to caller + amount = curvePool.remove_liquidity_one_coin( + shares, + coinIndex, + minReceived, + receiver + ); return amount; } From c69b16ae3cc5dd4aae6f1b8eab65d173d8d2e58b Mon Sep 17 00:00:00 2001 From: Rayerleier <1045643889@qq.com> Date: Wed, 30 Oct 2024 21:38:15 +0800 Subject: [PATCH 08/27] curve.sol:add authentication --- contracts/integrations/curve.sol | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol index 22704ec..efe595e 100644 --- a/contracts/integrations/curve.sol +++ b/contracts/integrations/curve.sol @@ -1,6 +1,7 @@ pragma solidity 0.8.18; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; interface IIntegration { function deposit(uint256 amount) external returns (uint256 shares); @@ -33,31 +34,35 @@ interface ICurveGauge { function withdraw(uint256 _value, bool _claim_rewards) external; } -contract CurveIntegration is IIntegration { +contract CurveIntegration is IIntegration, Ownable { address public curvePoolAddress; address public curveGaugeAddress; ICurvePool public curvePool; ICurveGauge public curveGauge; - constructor(address _curvePoolAddress, address _curveGaugeAddress) { + constructor( + address _curvePoolAddress, + address _curveGaugeAddress, + address _poolMerchant + ) Ownable() { curvePoolAddress = _curvePoolAddress; curveGaugeAddress = _curveGaugeAddress; curvePool = ICurvePool(curvePoolAddress); curveGauge = ICurveGauge(curveGaugeAddress); + transferOwnership(_poolMerchant); } function deposit( uint256 amount - ) external override returns (uint256 shares) { + ) external override onlyOwner returns (uint256 shares) { uint256[] memory amounts; // Create a dynamic array with 2 elements amounts[0] = 0; // Set the first element to 0 for the first coin amounts[1] = amount; // Set the second element to `amount` for the second coin uint256 minMintAmount = 0; // Set the acceptable minimum for LP tokens - address receiver = address(this); // The contract itself will receive the LP tokens // Step 1: Add liquidity to the Curve pool - shares = curvePool.add_liquidity(amounts, minMintAmount, receiver); + shares = curvePool.add_liquidity(amounts, minMintAmount, msg.sender); // Step 2: Approve the Gauge to spend LP tokens IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); @@ -70,7 +75,7 @@ contract CurveIntegration is IIntegration { function withdraw( uint256 shares - ) external override returns (uint256 amount) { + ) external override onlyOwner returns (uint256 amount) { // Step 1: Withdraw LP tokens from the Gauge curveGauge.withdraw(shares, false); From 777f83cbab8c15658cb553e55c6dc8a4ab7c025d Mon Sep 17 00:00:00 2001 From: scolear Date: Wed, 30 Oct 2024 18:45:13 +0100 Subject: [PATCH 09/27] WIP: curve integration --- contracts/DLCManager.sol | 8 +- contracts/PoolMerchant.sol | 202 ++++++----- contracts/integrations/curve.sol | 119 +++++-- contracts/interfaces/IIntegration.sol | 9 + contracts/mocks/MockERC20.sol | 28 ++ contracts/mocks/MockIntegration.sol | 69 ++++ test/DLCManager.test.js | 5 +- test/PoolMerchant.test.js | 473 ++++++++++++++++++++++++++ test/PoolMerchant_integration_test.js | 136 ++++++++ test/utils.js | 5 + 10 files changed, 937 insertions(+), 117 deletions(-) create mode 100644 contracts/interfaces/IIntegration.sol create mode 100644 contracts/mocks/MockERC20.sol create mode 100644 contracts/mocks/MockIntegration.sol create mode 100644 test/PoolMerchant.test.js create mode 100644 test/PoolMerchant_integration_test.js diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index 357b72e..2381869 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -361,12 +361,10 @@ contract DLCManager is // TODO: auth function setupPendingVault( - bytes32 _uuid, string calldata _taprootPubKey, string calldata _wdTxId - ) public onlyWhitelisted whenNotPaused { - if (dlcs[dlcIDsByUUID[_uuid]].uuid == _uuid) - revert DLCAlreadyExists(_uuid); + ) public onlyWhitelisted whenNotPaused returns (bytes32) { + bytes32 _uuid = generateUUID(msg.sender, _index); dlcs[_index] = DLCLink.DLC({ uuid: _uuid, protocolContract: msg.sender, // deprecated @@ -389,6 +387,8 @@ contract DLCManager is dlcIDsByUUID[_uuid] = _index; userVaults[msg.sender].push(_uuid); _index++; + + return _uuid; } /** diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 2d54975..e7ad557 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -18,23 +18,18 @@ import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "./DLCLinkLibrary.sol"; +import "./interfaces/IIntegration.sol"; interface IDLCManager { function setupPendingVault( - bytes32 _uuid, string calldata _taprootPubKey, string calldata _wdTxId - ) external; + ) external returns (bytes32); function withdraw(bytes32 uuid, uint256 amount) external; function getDLC(bytes32 uuid) external view returns (DLCLink.DLC memory); } -interface IIntegration { - function deposit(uint256 amount) external returns (uint256 shares); - function withdraw(uint256 shares) external returns (uint256 amount); -} - contract PoolMerchant is Initializable, ERC165Upgradeable, @@ -51,6 +46,7 @@ contract PoolMerchant is // STATE VARIABLES // //////////////////////////////////////////////////////////////// + bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant HARVESTER_ROLE = keccak256("HARVESTER_ROLE"); @@ -94,13 +90,19 @@ contract PoolMerchant is // EVENTS // //////////////////////////////////////////////////////////////// + event PendingVaultCreated( + bytes32 indexed uuid, + string taprootPubKey, + string withdrawalTxId + ); + event VaultWithdrawn(bytes32 indexed uuid, uint256 amount); event IntegrationAdded( address indexed integration, address[] supportedRewards ); event RewardTokenAdded(address indexed token); event SharesAllocated( - bytes32 indexed vaultId, + bytes32 indexed uuid, address indexed integration, uint256 shares ); @@ -111,12 +113,11 @@ contract PoolMerchant is uint256 amount ); event RewardsClaimed( - bytes32 indexed vaultId, + bytes32 indexed uuid, address indexed integration, address indexed rewardToken, uint256 amount ); - event VaultWithdrawn(bytes32 indexed vaultId, uint256 amount); //////////////////////////////////////////////////////////////// // CONSTRUCTOR // @@ -139,8 +140,6 @@ contract PoolMerchant is dlcManager = IDLCManager(_dlcManager); dlcBTC = IERC20(_dlcBTC); - - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } //////////////////////////////////////////////////////////////// @@ -148,22 +147,33 @@ contract PoolMerchant is //////////////////////////////////////////////////////////////// function createPendingVault( - bytes32 vaultId, string calldata taprootPubKey, string calldata withdrawalTxId - ) external onlyRole(OPERATOR_ROLE) whenNotPaused { + ) + external + onlyRole(ATTESTOR_ROLE) + nonReentrant + whenNotPaused + returns (bytes32) + { // Create pending vault in DLCManager - dlcManager.setupPendingVault(vaultId, taprootPubKey, withdrawalTxId); + bytes32 _uuid = dlcManager.setupPendingVault( + taprootPubKey, + withdrawalTxId + ); // Initialize our tracking (no need to store DLC data) - _vaults[vaultId].integrationShares[address(0)] = 0; // Just initialize the mapping + _vaults[_uuid].integrationShares[address(0)] = 0; // Just initialize the mapping + + emit PendingVaultCreated(_uuid, taprootPubKey, withdrawalTxId); + return _uuid; } function withdrawFromVault( - bytes32 vaultId, + bytes32 uuid, uint256 amount - ) external nonReentrant whenNotPaused { - DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + ) external onlyRole(ATTESTOR_ROLE) nonReentrant whenNotPaused { + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.uuid != bytes32(0), "Vault does not exist"); // First withdraw from any active integrations @@ -180,49 +190,83 @@ contract PoolMerchant is // 2. A harvester (automated or manual) collects these rewards // 3. The harvester calls this function to distribute rewards to vault holders function harvestRewards( - address integration, - address rewardToken, - uint256 amount + address integration ) external onlyRole(HARVESTER_ROLE) nonReentrant whenNotPaused { require(integrations[integration].isActive, "Integration not active"); - require(rewardTokens[rewardToken].isActive, "Invalid reward token"); - Integration storage integ = integrations[integration]; - uint256 totalShares = integ.totalShares; - // Get all vaults with shares in this integration + // Claim rewards from integration + uint256[] memory amounts = integ.strategy.claimRewards(); + address[] memory rewardAddresses = integ.strategy.getRewardTokens(); + require( + amounts.length == rewardAddresses.length, + "Invalid reward data" + ); + + uint256 totalShares = integ.totalShares; bytes32[] memory activeVaults = _getActiveVaultsForIntegration( integration ); - // Distribute ERC20 rewards proportionally - for (uint256 i = 0; i < activeVaults.length; i++) { - bytes32 vaultId = activeVaults[i]; - uint256 vaultShares = _vaults[vaultId].integrationShares[ - integration - ]; + // Distribute each reward token + for (uint256 i = 0; i < amounts.length; i++) { + address rewardAddress = rewardAddresses[i]; + uint256 amount = amounts[i]; + + // Skip unsupported reward tokens instead of reverting + if (!rewardTokens[rewardAddress].isActive) { + continue; + } - if (vaultShares > 0) { - UserReward storage reward = _vaults[vaultId].rewards[ + // Only distribute and emit events for supported tokens + for (uint256 j = 0; j < activeVaults.length; j++) { + bytes32 uuid = activeVaults[j]; + uint256 vaultShares = _vaults[uuid].integrationShares[ integration - ][rewardToken]; - uint256 vaultReward = (amount * vaultShares) / totalShares; - reward.pendingAmount += vaultReward; - reward.lastClaimedAt = block.timestamp; + ]; + + if (vaultShares > 0) { + UserReward storage reward = _vaults[uuid].rewards[ + integration + ][rewardAddress]; + uint256 vaultReward = (amount * vaultShares) / totalShares; + reward.pendingAmount += vaultReward; + reward.lastClaimedAt = block.timestamp; + } } + + emit RewardsHarvested( + integration, + rewardAddress, + msg.sender, + amount + ); } + } - emit RewardsHarvested(integration, rewardToken, msg.sender, amount); + function getPendingIntegrationRewards( + address integration + ) + external + view + returns (address[] memory tokens, uint256[] memory amounts) + { + require(integrations[integration].isActive, "Integration not active"); + Integration storage integ = integrations[integration]; + + tokens = integ.strategy.getRewardTokens(); + amounts = integ.strategy.getPendingRewards(); } // Claim rewards (ERC20s only) - // TODO: add auth + // TODO: add auth/a way for users to claim their rewards + // So, it would not be msg.sender who gets this function claimRewards( - bytes32 vaultId, + bytes32 uuid, address integration, address rewardToken ) external nonReentrant whenNotPaused { - UserReward storage reward = _vaults[vaultId].rewards[integration][ + UserReward storage reward = _vaults[uuid].rewards[integration][ rewardToken ]; require(reward.pendingAmount > 0, "No rewards to claim"); @@ -235,7 +279,7 @@ contract PoolMerchant is "Reward transfer failed" ); - emit RewardsClaimed(vaultId, integration, rewardToken, amount); + emit RewardsClaimed(uuid, integration, rewardToken, amount); } //////////////////////////////////////////////////////////////// @@ -271,17 +315,17 @@ contract PoolMerchant is // Allocate dlcBTC to an integration function allocateToIntegration( - bytes32 vaultId, + bytes32 uuid, address integration ) external onlyRole(OPERATOR_ROLE) nonReentrant whenNotPaused { require(integrations[integration].isActive, "Integration not active"); // Get current vault state - DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.valueMinted > 0, "Vault not funded"); // Calculate amount available to allocate - uint256 unallocated = dlc.valueMinted - _vaults[vaultId].totalAllocated; + uint256 unallocated = dlc.valueMinted - _vaults[uuid].totalAllocated; require(unallocated > 0, "Nothing to allocate"); // Approve and deposit to integration @@ -294,13 +338,13 @@ contract PoolMerchant is ); // Update share accounting - _vaults[vaultId].integrationShares[integration] += shares; - _vaults[vaultId].totalAllocated += unallocated; + _vaults[uuid].integrationShares[integration] += shares; + _vaults[uuid].totalAllocated += unallocated; integrations[integration].totalShares += shares; - _addVaultToIntegration(integration, vaultId); + _addVaultToIntegration(integration, uuid); - emit SharesAllocated(vaultId, integration, shares); + emit SharesAllocated(uuid, integration, shares); } function _isInActiveIntegrations( @@ -315,7 +359,7 @@ contract PoolMerchant is } function _withdrawFromIntegrations( - bytes32 vaultId, + bytes32 uuid, uint256 totalAmount ) internal { uint256 remainingAmount = totalAmount; @@ -326,9 +370,7 @@ contract PoolMerchant is i++ ) { address integration = activeIntegrations[i]; - uint256 shareAmount = _vaults[vaultId].integrationShares[ - integration - ]; + uint256 shareAmount = _vaults[uuid].integrationShares[integration]; if (shareAmount > 0) { Integration storage integ = integrations[integration]; @@ -338,14 +380,14 @@ contract PoolMerchant is if (withdrawAmount > 0) { uint256 received = integ.strategy.withdraw(withdrawAmount); remainingAmount -= received; - _vaults[vaultId].integrationShares[ + _vaults[uuid].integrationShares[ integration ] -= withdrawAmount; - _vaults[vaultId].totalAllocated -= received; // Update allocated tracking + _vaults[uuid].totalAllocated -= received; // Update allocated tracking integ.totalShares -= withdrawAmount; - if (_vaults[vaultId].integrationShares[integration] == 0) { - _removeVaultFromIntegration(integration, vaultId); + if (_vaults[uuid].integrationShares[integration] == 0) { + _removeVaultFromIntegration(integration, uuid); } } } @@ -392,18 +434,18 @@ contract PoolMerchant is //////////////////////////////////////////////////////////////// function getVaultShares( - bytes32 vaultId, + bytes32 uuid, address integration ) external view returns (uint256) { - return _vaults[vaultId].integrationShares[integration]; + return _vaults[uuid].integrationShares[integration]; } function getVaultReward( - bytes32 vaultId, + bytes32 uuid, address integration, address rewardToken ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { - UserReward storage reward = _vaults[vaultId].rewards[integration][ + UserReward storage reward = _vaults[uuid].rewards[integration][ rewardToken ]; return (reward.lastClaimedAt, reward.pendingAmount); @@ -411,24 +453,22 @@ contract PoolMerchant is // Helper function to get total shares in an integration for a vault function getVaultTotalShares( - bytes32 vaultId + bytes32 uuid ) external view returns (uint256 totalShares) { for (uint256 i = 0; i < activeIntegrations.length; i++) { - totalShares += _vaults[vaultId].integrationShares[ + totalShares += _vaults[uuid].integrationShares[ activeIntegrations[i] ]; } } - function getUnallocatedAmount( - bytes32 vaultId - ) public view returns (uint256) { - DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); - return dlc.valueMinted - _vaults[vaultId].totalAllocated; + function getUnallocatedAmount(bytes32 uuid) public view returns (uint256) { + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); + return dlc.valueMinted - _vaults[uuid].totalAllocated; } function getVaultAllocationDetails( - bytes32 vaultId + bytes32 uuid ) external view @@ -438,31 +478,29 @@ contract PoolMerchant is uint256 unallocated ) { - DLCLink.DLC memory dlc = dlcManager.getDLC(vaultId); + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); totalMinted = dlc.valueMinted; - totalAllocated = _vaults[vaultId].totalAllocated; + totalAllocated = _vaults[uuid].totalAllocated; unallocated = totalMinted - totalAllocated; } function _addVaultToIntegration( address integration, - bytes32 vaultId + bytes32 uuid ) internal { - if (_vaultIndices[integration][vaultId] == 0) { + if (_vaultIndices[integration][uuid] == 0) { // 0 means not found - _integrationVaults[integration].push(vaultId); - _vaultIndices[integration][vaultId] = _integrationVaults[ - integration - ].length; + _integrationVaults[integration].push(uuid); + _vaultIndices[integration][uuid] = _integrationVaults[integration] + .length; } } - // Add this helper function function _removeVaultFromIntegration( address integration, - bytes32 vaultId + bytes32 uuid ) internal { - uint256 index = _vaultIndices[integration][vaultId]; + uint256 index = _vaultIndices[integration][uuid]; if (index > 0) { // If vault exists in array index--; // Convert from 1-based to 0-based index @@ -478,7 +516,7 @@ contract PoolMerchant is // Remove last element _integrationVaults[integration].pop(); - delete _vaultIndices[integration][vaultId]; + delete _vaultIndices[integration][uuid]; } } diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol index efe595e..79982cf 100644 --- a/contracts/integrations/curve.sol +++ b/contracts/integrations/curve.sol @@ -1,13 +1,9 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.18; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; - -interface IIntegration { - function deposit(uint256 amount) external returns (uint256 shares); - - function withdraw(uint256 shares) external returns (uint256 amount); -} +import "../interfaces/IIntegration.sol"; interface ICurvePool { function add_liquidity( @@ -22,16 +18,22 @@ interface ICurvePool { uint256 _min_received, address _receiver ) external returns (uint256); + + function coins(uint256 i) external view returns (address); } +// NOTE: https://curve.readthedocs.io/dao-gauges.html#liquiditygaugev3 +// @Rayerleier: I am not fully sure all of these will work. Let's try and test tomorrow. interface ICurveGauge { - function deposit( - uint256 _value, - address _user, - bool _claim_rewards - ) external; - + function deposit(uint256 _value, address _user) external; function withdraw(uint256 _value, bool _claim_rewards) external; + function claim_rewards(address _addr) external; + function reward_tokens(uint256 i) external view returns (address); + function reward_count() external view returns (uint256); + function claimable_reward( + address _user, + address _reward_token + ) external view returns (uint256); } contract CurveIntegration is IIntegration, Ownable { @@ -39,25 +41,40 @@ contract CurveIntegration is IIntegration, Ownable { address public curveGaugeAddress; ICurvePool public curvePool; ICurveGauge public curveGauge; + address public dlcBTC; + int128 public dlcBTCIndex; constructor( address _curvePoolAddress, address _curveGaugeAddress, - address _poolMerchant + address _poolMerchant, + address _dlcBTC ) Ownable() { curvePoolAddress = _curvePoolAddress; curveGaugeAddress = _curveGaugeAddress; curvePool = ICurvePool(curvePoolAddress); curveGauge = ICurveGauge(curveGaugeAddress); + dlcBTC = _dlcBTC; + + // Find dlcBTC index in pool + if (curvePool.coins(0) == dlcBTC) { + dlcBTCIndex = 0; + } else if (curvePool.coins(1) == dlcBTC) { + dlcBTCIndex = 1; + } else { + revert("dlcBTC not found in pool"); + } + transferOwnership(_poolMerchant); } + // NOTE: we might have to change how shares/amounts translate... + // since dlcBTC is 8 decimals and the curve pool is 18 decimals function deposit( uint256 amount ) external override onlyOwner returns (uint256 shares) { - uint256[] memory amounts; // Create a dynamic array with 2 elements - amounts[0] = 0; // Set the first element to 0 for the first coin - amounts[1] = amount; // Set the second element to `amount` for the second coin + uint256[] memory amounts = new uint256[](2); + amounts[uint256(uint128(dlcBTCIndex))] = amount; uint256 minMintAmount = 0; // Set the acceptable minimum for LP tokens @@ -68,7 +85,7 @@ contract CurveIntegration is IIntegration, Ownable { IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); // Step 3: Deposit LP tokens into Curve Gauge for rewards - curveGauge.deposit(shares, msg.sender, false); + curveGauge.deposit(shares, msg.sender); return shares; } @@ -76,21 +93,69 @@ contract CurveIntegration is IIntegration, Ownable { function withdraw( uint256 shares ) external override onlyOwner returns (uint256 amount) { - // Step 1: Withdraw LP tokens from the Gauge - curveGauge.withdraw(shares, false); - - // Step 2: Withdraw the second coin from Curve pool - int128 coinIndex = 1; // Set to 1 for the second coin - uint256 minReceived = 0; // Minimum amount of coin to receive - address receiver = msg.sender; + // Step 1: Withdraw LP tokens from gauge + curveGauge.withdraw(shares, true); // Claim rewards during withdrawal + // Step 2: Remove liquidity for dlcBTC amount = curvePool.remove_liquidity_one_coin( shares, - coinIndex, - minReceived, - receiver + dlcBTCIndex, + 0, + msg.sender ); return amount; } + + function claimRewards() + external + override + onlyOwner + returns (uint256[] memory amounts) + { + curveGauge.claim_rewards(address(this)); + + uint256 rewardCount = curveGauge.reward_count(); + amounts = new uint256[](rewardCount); + + for (uint256 i = 0; i < rewardCount; i++) { + address rewardToken = curveGauge.reward_tokens(i); + amounts[i] = IERC20(rewardToken).balanceOf(address(this)); + if (amounts[i] > 0) { + IERC20(rewardToken).transfer(msg.sender, amounts[i]); + } + } + } + + function getRewardTokens() + external + view + override + returns (address[] memory tokens) + { + uint256 rewardCount = curveGauge.reward_count(); + tokens = new address[](rewardCount); + + for (uint256 i = 0; i < rewardCount; i++) { + tokens[i] = curveGauge.reward_tokens(i); + } + } + + function getPendingRewards() + external + view + override + returns (uint256[] memory amounts) + { + uint256 rewardCount = curveGauge.reward_count(); + amounts = new uint256[](rewardCount); + + for (uint256 i = 0; i < rewardCount; i++) { + address rewardToken = curveGauge.reward_tokens(i); + amounts[i] = curveGauge.claimable_reward( + address(this), + rewardToken + ); + } + } } diff --git a/contracts/interfaces/IIntegration.sol b/contracts/interfaces/IIntegration.sol new file mode 100644 index 0000000..3637bdd --- /dev/null +++ b/contracts/interfaces/IIntegration.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; +interface IIntegration { + function deposit(uint256 amount) external returns (uint256 shares); + function withdraw(uint256 shares) external returns (uint256 amount); + function claimRewards() external returns (uint256[] memory amounts); + function getRewardTokens() external view returns (address[] memory); + function getPendingRewards() external view returns (uint256[] memory); +} diff --git a/contracts/mocks/MockERC20.sol b/contracts/mocks/MockERC20.sol new file mode 100644 index 0000000..5d26065 --- /dev/null +++ b/contracts/mocks/MockERC20.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + uint8 private _decimals; + + constructor( + string memory name, + string memory symbol, + uint8 decimalsValue + ) ERC20(name, symbol) { + _decimals = decimalsValue; + } + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } + + function burn(address account, uint256 amount) public { + _burn(account, amount); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/contracts/mocks/MockIntegration.sol b/contracts/mocks/MockIntegration.sol new file mode 100644 index 0000000..df3ca68 --- /dev/null +++ b/contracts/mocks/MockIntegration.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../interfaces/IIntegration.sol"; + +contract MockIntegration is IIntegration { + address[] public rewardTokens; + mapping(address => uint256) public pendingRewards; + + constructor(address[] memory _rewardTokens) { + rewardTokens = _rewardTokens; + } + + function deposit(uint256 amount) external override returns (uint256) { + return amount; + } + + function withdraw(uint256 shares) external override returns (uint256) { + return shares; + } + + function claimRewards() + external + override + returns (uint256[] memory amounts) + { + amounts = new uint256[](rewardTokens.length); + for (uint256 i = 0; i < rewardTokens.length; i++) { + amounts[i] = pendingRewards[rewardTokens[i]]; + pendingRewards[rewardTokens[i]] = 0; + + // Transfer rewards to caller + IERC20(rewardTokens[i]).transfer(msg.sender, amounts[i]); + } + } + + function getRewardTokens() + external + view + override + returns (address[] memory) + { + return rewardTokens; + } + + function getPendingRewards() + external + view + override + returns (uint256[] memory amounts) + { + amounts = new uint256[](rewardTokens.length); + for (uint256 i = 0; i < rewardTokens.length; i++) { + amounts[i] = pendingRewards[rewardTokens[i]]; + } + } + + // Test helper function + function mockRewards(uint256[] memory amounts) external { + require( + amounts.length == rewardTokens.length, + "Invalid amounts length" + ); + for (uint256 i = 0; i < rewardTokens.length; i++) { + pendingRewards[rewardTokens[i]] = amounts[i]; + } + } +} diff --git a/test/DLCManager.test.js b/test/DLCManager.test.js index b10b8ab..f1a7427 100644 --- a/test/DLCManager.test.js +++ b/test/DLCManager.test.js @@ -7,12 +7,9 @@ const { getSignatures, setSigners, getMultipleSignaturesForSameAttestorAndMessage, + whitelistAddress, } = require('./utils'); -async function whitelistAddress(dlcManager, user) { - await dlcManager.whitelistAddress(user.address); -} - describe('DLCManager', () => { let dlcManager, dlcBtc, uuid; let accounts, deployer, user, randomAccount, anotherAccount, protocol; diff --git a/test/PoolMerchant.test.js b/test/PoolMerchant.test.js new file mode 100644 index 0000000..9f357ce --- /dev/null +++ b/test/PoolMerchant.test.js @@ -0,0 +1,473 @@ +const { expect } = require('chai'); +const { ethers, upgrades } = require('hardhat'); +const hardhat = require('hardhat'); + +const { getSignatures, setSigners, whitelistAddress } = require('./utils'); + +describe('PoolMerchant', () => { + let poolMerchant; + let dlcManager; + let dlcBtc; + let curveIntegration; + let curvePool; + let curveGauge; + let deployer; + let attestor_1; + let attestor_2; + let attestor_3; + let attestors; + let operator; + let harvester; + let user; + let mockIntegration; + let mockRewardToken; + + const ATTESTOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes('ATTESTOR_ROLE') + ); + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes('OPERATOR_ROLE') + ); + const HARVESTER_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes('HARVESTER_ROLE') + ); + + let btcFeeRecipient = '0x000001'; + + beforeEach(async function () { + [ + deployer, + attestor_1, + attestor_2, + attestor_3, + operator, + harvester, + user, + ] = await ethers.getSigners(); + + attestors = [attestor_1, attestor_2, attestor_3]; + + const DLCBTC = await ethers.getContractFactory('DLCBTC', deployer); + dlcBtc = await hardhat.upgrades.deployProxy(DLCBTC); + await dlcBtc.deployed(); + + // DLCManager + const DLCManager = await ethers.getContractFactory('DLCManager'); + dlcManager = await hardhat.upgrades.deployProxy(DLCManager, [ + deployer.address, + deployer.address, + 3, + dlcBtc.address, + btcFeeRecipient, + ]); + await dlcManager.deployed(); + await dlcBtc.transferOwnership(dlcManager.address); + await setSigners(dlcManager, attestors); + + // Deploy Mock Integration + + // Deploy Mock Reward Token + const MockERC20 = await ethers.getContractFactory('MockERC20'); + mockRewardToken = await MockERC20.deploy('Mock Reward', 'MRWD', 18); + + const MockIntegration = + await ethers.getContractFactory('MockIntegration'); + mockIntegration = await MockIntegration.deploy([ + mockRewardToken.address, + ]); + + // Deploy PoolMerchant + const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + dlcManager.address, + dlcBtc.address, + deployer.address, + ]); + + // Setup roles + await poolMerchant.grantRole(ATTESTOR_ROLE, attestor_1.address); + await poolMerchant.grantRole(OPERATOR_ROLE, operator.address); + await poolMerchant.grantRole(HARVESTER_ROLE, harvester.address); + + await whitelistAddress(dlcManager, poolMerchant); + + // Transfer CurveIntegration ownership to PoolMerchant + // await curveIntegration.transferOwnership(poolMerchant.address); + }); + + describe('Initialization', function () { + it('should initialize with correct state', async function () { + expect(await poolMerchant.dlcManager()).to.equal( + dlcManager.address + ); + expect(await poolMerchant.dlcBTC()).to.equal(dlcBtc.address); + expect( + await poolMerchant.hasRole(ATTESTOR_ROLE, attestor_1.address) + ).to.be.true; + expect(await poolMerchant.hasRole(OPERATOR_ROLE, operator.address)) + .to.be.true; + expect( + await poolMerchant.hasRole(HARVESTER_ROLE, harvester.address) + ).to.be.true; + }); + }); + + describe('Vault Creation', function () { + it('should create a pending vault', async function () { + const taprootPubKey = 'taproot123'; + const withdrawalTxId = 'tx123'; + + const tx = await poolMerchant + .connect(attestor_1) + .createPendingVault(taprootPubKey, withdrawalTxId); + await expect(tx).to.not.be.reverted; + + const receipt = await tx.wait(); + const event = receipt.events.find( + (event) => event.event === 'PendingVaultCreated' + ); + const vaultId = event.args.uuid; + + // Verify vault was created in DLCManager + const UUID = (await dlcManager.getDLC(vaultId)).uuid; + expect(UUID).to.equal(vaultId); + }); + }); + + describe('Reward Token Management', function () { + it('should add reward token', async function () { + await expect( + poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address) + ) + .to.emit(poolMerchant, 'RewardTokenAdded') + .withArgs(mockRewardToken.address); + + const rewardToken = await poolMerchant.rewardTokens( + mockRewardToken.address + ); + expect(rewardToken.tokenAddress).to.equal(mockRewardToken.address); + expect(rewardToken.isActive).to.be.true; + }); + + it('should not add same reward token twice', async function () { + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + await expect( + poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address) + ).to.be.revertedWith('Token already added'); + }); + + it('should only allow admin to add reward tokens', async function () { + await expect( + poolMerchant + .connect(user) + .addRewardToken(mockRewardToken.address) + ).to.be.reverted; + }); + }); + + describe('Integration Management', function () { + beforeEach(async function () { + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + }); + + it('should set integration with supported rewards', async function () { + await expect( + poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]) + ) + .to.emit(poolMerchant, 'IntegrationAdded') + .withArgs(mockIntegration.address, [mockRewardToken.address]); + + const integration = await poolMerchant.integrations( + mockIntegration.address + ); + expect(integration.isActive).to.be.true; + expect(integration.totalShares).to.equal(0); + expect(integration.strategy).to.equal(mockIntegration.address); + }); + + it('should not allow setting integration with unsupported reward token', async function () { + await expect( + poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + ethers.constants.AddressZero, + ]) + ).to.be.revertedWith('Invalid reward token'); + }); + + it('should only allow admin to set integration', async function () { + await expect( + poolMerchant + .connect(user) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]) + ).to.be.reverted; + }); + }); + + describe('Vault Creation and Management', function () { + let vaultId; + const taprootPubKey = 'taproot123'; + const withdrawalTxId = 'tx123'; + const valueLocked = 1000000; + + beforeEach(async function () { + const tx = await poolMerchant + .connect(attestor_1) + .createPendingVault(taprootPubKey, withdrawalTxId); + const receipt = await tx.wait(); + vaultId = receipt.events.find( + (event) => event.event === 'PendingVaultCreated' + ).args.uuid; + }); + + it('should allocate to integration', async function () { + // Setup integration + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + await poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]); + + const signatureBytesForFunding = await getSignatures( + { + uuid: vaultId, + btcTxId: withdrawalTxId, + functionString: 'set-status-funded', + newLockedAmount: valueLocked, + }, + attestors, + 3 + ); + const tx3 = await dlcManager + .connect(attestor_1) + .setStatusFunded( + vaultId, + withdrawalTxId, + signatureBytesForFunding, + valueLocked + ); + await tx3.wait(); + + // Allocate + await expect( + poolMerchant + .connect(operator) + .allocateToIntegration(vaultId, mockIntegration.address) + ) + .to.emit(poolMerchant, 'SharesAllocated') + .withArgs(vaultId, mockIntegration.address, valueLocked); // Assuming 1:1 share ratio + + const shares = await poolMerchant.getVaultShares( + vaultId, + mockIntegration.address + ); + expect(shares).to.equal(valueLocked); + }); + + it('should not allocate unfunded vault', async function () { + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + await poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]); + + await expect( + poolMerchant + .connect(operator) + .allocateToIntegration(vaultId, mockIntegration.address) + ).to.be.revertedWith('Vault not funded'); + }); + }); + + describe('Reward Harvesting and Claims', function () { + let vaultId; + let mockRewardToken2; + const withdrawalTxId = 'tx123'; + const valueLocked = 1000000; + const initialFunding = ethers.utils.parseUnits('1', 8); + + beforeEach(async function () { + // Create and fund vault + const tx = await poolMerchant + .connect(attestor_1) + .createPendingVault('taproot123', 'tx123'); + const receipt = await tx.wait(); + vaultId = receipt.events.find( + (e) => e.event === 'PendingVaultCreated' + ).args.uuid; + const signatureBytesForFunding = await getSignatures( + { + uuid: vaultId, + btcTxId: withdrawalTxId, + functionString: 'set-status-funded', + newLockedAmount: initialFunding, + }, + attestors, + 3 + ); + const tx3 = await dlcManager + .connect(attestor_1) + .setStatusFunded( + vaultId, + withdrawalTxId, + signatureBytesForFunding, + initialFunding + ); + await tx3.wait(); + + // Deploy second mock reward token (unsupported) + const MockERC20 = await ethers.getContractFactory('MockERC20'); + mockRewardToken2 = await MockERC20.deploy( + 'Mock Reward 2', + 'MRWD2', + 18 + ); + + // Deploy integration with both reward tokens + const MockIntegration = + await ethers.getContractFactory('MockIntegration'); + mockIntegration = await MockIntegration.deploy([ + mockRewardToken.address, + mockRewardToken2.address, + ]); + + // Setup supported reward token and integration + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + await poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]); + await poolMerchant + .connect(operator) + .allocateToIntegration(vaultId, mockIntegration.address); + + // Fund mock integration with rewards + await mockRewardToken.mint( + mockIntegration.address, + ethers.utils.parseEther('100') + ); + await mockRewardToken2.mint( + mockIntegration.address, + ethers.utils.parseEther('100') + ); + }); + + it('should harvest and distribute only supported rewards', async function () { + // Mock pending rewards in integration + const supportedAmount = ethers.utils.parseEther('10'); + const unsupportedAmount = ethers.utils.parseEther('5'); + await mockIntegration.mockRewards([ + supportedAmount, + unsupportedAmount, + ]); + + await expect( + poolMerchant + .connect(harvester) + .harvestRewards(mockIntegration.address) + ) + .to.emit(poolMerchant, 'RewardsHarvested') + .withArgs( + mockIntegration.address, + mockRewardToken.address, + harvester.address, + supportedAmount + ); + + // Check only supported token was distributed + const [_, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + mockIntegration.address, + mockRewardToken.address + ); + expect(pendingAmount).to.equal(supportedAmount); + + // Verify unsupported token was claimed but not distributed + expect( + await mockRewardToken2.balanceOf(poolMerchant.address) + ).to.equal(unsupportedAmount); + }); + + it('should allow users to claim rewards', async function () { + await mockIntegration.mockRewards([ + ethers.utils.parseEther('10'), + ethers.utils.parseEther('5'), + ]); + await poolMerchant + .connect(harvester) + .harvestRewards(mockIntegration.address); + + await expect( + poolMerchant + .connect(user) + .claimRewards( + vaultId, + mockIntegration.address, + mockRewardToken.address + ) + ) + .to.emit(poolMerchant, 'RewardsClaimed') + .withArgs( + vaultId, + mockIntegration.address, + mockRewardToken.address, + ethers.utils.parseEther('10') + ); + + // Verify reward was claimed + const [_, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + mockIntegration.address, + mockRewardToken.address + ); + expect(pendingAmount).to.equal(0); + expect(await mockRewardToken.balanceOf(user.address)).to.equal( + ethers.utils.parseEther('10') + ); + }); + + it('should handle rewards when no tokens are supported', async function () { + // Deploy integration with only unsupported reward + const newIntegration = await ( + await ethers.getContractFactory('MockIntegration') + ).deploy([mockRewardToken2.address]); + + await poolMerchant + .connect(deployer) + .setIntegration(newIntegration.address, []); + await mockRewardToken2.mint( + newIntegration.address, + ethers.utils.parseEther('10') + ); + await newIntegration.mockRewards([ethers.utils.parseEther('10')]); + + // Should not revert but no rewards distributed + await poolMerchant + .connect(harvester) + .harvestRewards(newIntegration.address); + }); + }); +}); diff --git a/test/PoolMerchant_integration_test.js b/test/PoolMerchant_integration_test.js new file mode 100644 index 0000000..f78aeac --- /dev/null +++ b/test/PoolMerchant_integration_test.js @@ -0,0 +1,136 @@ +const hre = require('hardhat'); +const { ethers, upgrades } = require('hardhat'); + +async function main() { + console.log('\nšŸš€ Starting happy path integration test...'); + + // Configure network and addresses + const MAINNET_ADDRESSES = { + DLC_MANAGER: '0x...', // Add real address + DLC_BTC: '0x...', // Add real address + CURVE_POOL: '0x...', // Add real address + CURVE_GAUGE: '0x...', // Add real address + CRV_TOKEN: '0x...', // Add real address + }; + + // Impersonate necessary accounts + const accounts = await ethers.getSigners(); + const deployer = accounts[0]; + const attestor = await ethers.getImpersonatedSigner('0x...'); // Add real attestor + const operator = await ethers.getImpersonatedSigner('0x...'); // Add real operator + const harvester = await ethers.getImpersonatedSigner('0x...'); // Add real harvester + + console.log('\nšŸ“ Getting contract instances...'); + const dlcManager = await ethers.getContractAt( + 'DLCManager', + MAINNET_ADDRESSES.DLC_MANAGER + ); + const dlcBTC = await ethers.getContractAt( + 'DLCBTC', + MAINNET_ADDRESSES.DLC_BTC + ); + + // Deploy PoolMerchant + console.log('\nšŸ—ļø Deploying PoolMerchant...'); + const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + MAINNET_ADDRESSES.DLC_MANAGER, + MAINNET_ADDRESSES.DLC_BTC, + deployer.address, + ]); + await poolMerchant.deployed(); + console.log('PoolMerchant deployed to:', poolMerchant.address); + + // Deploy CurveIntegration + console.log('\nšŸ—ļø Deploying CurveIntegration...'); + const CurveIntegration = + await ethers.getContractFactory('CurveIntegration'); + const curveIntegration = await CurveIntegration.deploy( + MAINNET_ADDRESSES.CURVE_POOL, + MAINNET_ADDRESSES.CURVE_GAUGE, + poolMerchant.address, + MAINNET_ADDRESSES.DLC_BTC + ); + await curveIntegration.deployed(); + console.log('CurveIntegration deployed to:', curveIntegration.address); + + // Setup roles + console.log('\nšŸ”‘ Setting up roles...'); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + attestor.address + ); + await poolMerchant.grantRole( + await poolMerchant.OPERATOR_ROLE(), + operator.address + ); + await poolMerchant.grantRole( + await poolMerchant.HARVESTER_ROLE(), + harvester.address + ); + + // Add CRV as reward token + console.log('\nšŸŖ™ Adding CRV as reward token...'); + await poolMerchant.addRewardToken(MAINNET_ADDRESSES.CRV_TOKEN); + + // Setup integration + console.log('\nšŸ”„ Setting up CurveIntegration...'); + await poolMerchant.setIntegration(curveIntegration.address, [ + MAINNET_ADDRESSES.CRV_TOKEN, + ]); + + // Create vault + console.log('\nšŸ“¦ Creating vault...'); + const tx = await poolMerchant + .connect(attestor) + .createPendingVault('taproot123', 'tx123'); + const receipt = await tx.wait(); + const vaultId = receipt.events.find( + (e) => e.event === 'PendingVaultCreated' + ).args.uuid; + console.log('Vault created with ID:', vaultId); + + // Fund vault (mock) + console.log('\nšŸ’° Funding vault...'); + const fundAmount = ethers.utils.parseUnits('1', 8); // 1 BTC + await dlcManager.connect(attestor).mockFundVault(vaultId, fundAmount); + + // Allocate to Curve + console.log('\nšŸ“ˆ Allocating to Curve...'); + await poolMerchant + .connect(operator) + .allocateToIntegration(vaultId, curveIntegration.address); + const shares = await poolMerchant.getVaultShares( + vaultId, + curveIntegration.address + ); + console.log('Allocated shares:', shares.toString()); + + // Wait for some blocks to accrue rewards + console.log('\nā³ Mining blocks to accrue rewards...'); + await hre.network.provider.send('hardhat_mine', ['0x100']); // Mine 256 blocks + + // Harvest rewards + console.log('\nšŸŒ¾ Harvesting rewards...'); + await poolMerchant + .connect(harvester) + .harvestRewards(curveIntegration.address); + const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + curveIntegration.address, + MAINNET_ADDRESSES.CRV_TOKEN + ); + console.log( + 'Pending CRV rewards:', + ethers.utils.formatEther(pendingAmount) + ); + + console.log('\nāœ… Happy path integration test complete!'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/utils.js b/test/utils.js index eabe75a..c4c88b9 100644 --- a/test/utils.js +++ b/test/utils.js @@ -95,8 +95,13 @@ async function getMultipleSignaturesForSameAttestorAndMessage( return signatureBytes; } +async function whitelistAddress(dlcManager, user) { + await dlcManager.whitelistAddress(user.address); +} + module.exports = { getSignatures, setSigners, getMultipleSignaturesForSameAttestorAndMessage, + whitelistAddress, }; From 44341bf3b9631ab6c4a2664df5deac4f44fe711f Mon Sep 17 00:00:00 2001 From: scolear Date: Wed, 30 Oct 2024 21:44:39 +0100 Subject: [PATCH 10/27] feat: working full forked flow --- contracts/DLCManager.sol | 15 +- contracts/PoolMerchant.sol | 9 +- contracts/integrations/curve.md | 114 ++++++++++++ contracts/integrations/curve.sol | 18 +- scripts/pool_merchant_flow.js | 246 ++++++++++++++++++++++++++ test/PoolMerchant_integration_test.js | 136 -------------- 6 files changed, 388 insertions(+), 150 deletions(-) create mode 100644 contracts/integrations/curve.md create mode 100644 scripts/pool_merchant_flow.js delete mode 100644 test/PoolMerchant_integration_test.js diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index 2381869..e483e41 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -69,7 +69,11 @@ contract DLCManager is bool public porEnabled; AggregatorV3Interface public dlcBTCPoRFeed; mapping(address => mapping(bytes32 => bool)) private _seenSigners; - uint256[39] __gap; + + // TODO: FIXME: NOTE: Remove this for production + bool public skipSignatureVerification; // Add state variable + + uint256[38] __gap; //////////////////////////////////////////////////////////////// // ERRORS // @@ -233,6 +237,10 @@ contract DLCManager is bytes memory message, bytes[] memory signatures ) internal { + // TODO: FIXME: NOTE: Remove this for production + if (skipSignatureVerification) { + return; + } if (signatures.length < _threshold) revert NotEnoughSignatures(); bytes32 prefixedMessageHash = ECDSAUpgradeable.toEthSignedMessageHash( @@ -758,4 +766,9 @@ contract DLCManager is dlcBTCPoRFeed = feed; emit SetDlcBTCPoRFeed(feed); } + + // Add function to toggle (only owner/admin) + function setSkipSignatureVerification(bool skip) external onlyAdmin { + skipSignatureVerification = skip; + } } diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index e7ad557..8bf0d8f 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -328,11 +328,10 @@ contract PoolMerchant is uint256 unallocated = dlc.valueMinted - _vaults[uuid].totalAllocated; require(unallocated > 0, "Nothing to allocate"); - // Approve and deposit to integration - require( - dlcBTC.approve(address(integration), unallocated), - "Approval failed" - ); + // First approve the integration to spend dlcBTC + require(dlcBTC.approve(integration, unallocated), "Approval failed"); + + // Then deposit through the integration uint256 shares = integrations[integration].strategy.deposit( unallocated ); diff --git a/contracts/integrations/curve.md b/contracts/integrations/curve.md new file mode 100644 index 0000000..7839f3d --- /dev/null +++ b/contracts/integrations/curve.md @@ -0,0 +1,114 @@ +# CurveIntegration Deposit Flow + +## Initial State + +- PoolMerchant has dlcBTC tokens +- PoolMerchant approves CurveIntegration to spend dlcBTC + +## Step 1: Add Liquidity to Pool + +```solidity +// Prepare amounts array for curve pool (e.g., for a 2-token pool) +uint256[] memory amounts = new uint256[](2); +amounts[uint256(uint128(dlcBTCIndex))] = amount; +// If dlcBTC is the second token (index 1), this means: +// amounts = [0, amount] + +// Approve curve pool to spend our dlcBTC +IERC20(dlcBTC).approve(curvePoolAddress, amount); + +// Add liquidity to pool +// Returns LP tokens representing our share of the pool +shares = curvePool.add_liquidity(amounts, 0, address(this)); +``` + +## Step 2: Deposit LP Tokens to Gauge + +```solidity +// Approve gauge to spend LP tokens +IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); + +// Deposit LP tokens into gauge +// This is what enables us to earn CRV rewards +curveGauge.deposit(shares, address(this)); +``` + +## Flow of Tokens + +1. dlcBTC: PoolMerchant -> CurveIntegration -> Curve Pool +2. LP Tokens: Curve Pool -> CurveIntegration -> Curve Gauge + +## Results + +- Our dlcBTC is in the Curve Pool +- Our LP tokens are in the Curve Gauge +- We'll earn CRV rewards based on our gauge deposit +- The shares returned represent our proportional ownership of the pool + +## Key Points + +- All token movements require prior approvals +- We use minMintAmount = 0 (no slippage protection currently) +- We keep LP tokens in the gauge to earn rewards +- Our shares accounting in PoolMerchant matches the LP tokens we have in the gauge + +# CurveIntegration Withdraw Flow + +## Initial State + +- Our LP tokens are in the Curve Gauge +- We want to get back dlcBTC +- PoolMerchant calls withdraw with the number of shares (LP tokens) to withdraw + +## Step 1: Withdraw from Gauge + +```solidity +// Withdraw LP tokens from gauge +// claim_rewards = true means we'll also get any pending CRV rewards +curveGauge.withdraw(shares, true); +``` + +## Step 2: Remove Liquidity from Pool + +```solidity +// Remove liquidity for a single coin (dlcBTC) +amount = curvePool.remove_liquidity_one_coin( + shares, // amount of LP tokens to burn + dlcBTCIndex, // index of token we want back (dlcBTC) + 0, // minimum amount to accept + msg.sender // recipient (PoolMerchant) +); +``` + +## Flow of Tokens + +1. LP Tokens: Curve Gauge -> CurveIntegration -> Curve Pool +2. dlcBTC: Curve Pool -> PoolMerchant + +## Results + +- We get back dlcBTC tokens +- Our LP tokens are burned +- We may receive any accrued CRV rewards +- The `amount` returned is how many dlcBTC tokens we received + +## Key Points + +- No approvals needed for withdrawals +- We use minReceived = 0 (no slippage protection currently) +- We might get fewer dlcBTC back than we put in due to: + - Pool fees + - Price changes + - Slippage +- Using remove_liquidity_one_coin is more gas efficient than remove_liquidity +- Any pending rewards are claimed automatically if claim_rewards = true + +## Notes on Accounting + +- PoolMerchant updates its accounting based on the returned amount: + +```solidity +_vaults[uuid].integrationShares[integration] -= withdrawAmount; +_vaults[uuid].totalAllocated -= received; +integ.totalShares -= withdrawAmount; +``` diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol index 79982cf..64a4d54 100644 --- a/contracts/integrations/curve.sol +++ b/contracts/integrations/curve.sol @@ -73,19 +73,21 @@ contract CurveIntegration is IIntegration, Ownable { function deposit( uint256 amount ) external override onlyOwner returns (uint256 shares) { + // The dlcBTC tokens are still in the PoolMerchant, which has approved us to spend them + + // First approve the curve pool to spend the dlcBTC that we'll transferFrom + IERC20(dlcBTC).approve(curvePoolAddress, amount); + uint256[] memory amounts = new uint256[](2); amounts[uint256(uint128(dlcBTCIndex))] = amount; - uint256 minMintAmount = 0; // Set the acceptable minimum for LP tokens + // Transfer the dlcBTC from PoolMerchant and add liquidity to the pool + IERC20(dlcBTC).transferFrom(msg.sender, address(this), amount); + shares = curvePool.add_liquidity(amounts, 0, address(this)); - // Step 1: Add liquidity to the Curve pool - shares = curvePool.add_liquidity(amounts, minMintAmount, msg.sender); - - // Step 2: Approve the Gauge to spend LP tokens + // Approve and deposit LP tokens into gauge IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); - - // Step 3: Deposit LP tokens into Curve Gauge for rewards - curveGauge.deposit(shares, msg.sender); + curveGauge.deposit(shares, address(this)); return shares; } diff --git a/scripts/pool_merchant_flow.js b/scripts/pool_merchant_flow.js new file mode 100644 index 0000000..36f0358 --- /dev/null +++ b/scripts/pool_merchant_flow.js @@ -0,0 +1,246 @@ +const hre = require('hardhat'); +const { ethers, upgrades } = require('hardhat'); + +const { getSignatures, whitelistAddress } = require('../test/utils'); + +async function fundAccount(address) { + await hre.network.provider.send('hardhat_setBalance', [ + address, + '0x2000000000000000000', // 2 ETH + ]); +} + +async function main() { + // Compile contracts + console.log('\nšŸ”Ø Compiling contracts...'); + await hre.run('compile'); + + console.log('\nšŸš€ Starting happy path integration test...'); + + // Configure network and addresses + const MAINNET_ADDRESSES = { + DLC_MANAGER: '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', + DLC_BTC: '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', + CURVE_POOL: '0xe957cE03cCdd88f02ed8b05C9a3A28ABEf38514A', + CURVE_GAUGE: '0x02b8e750E68cb648dB2c2ac4BBb47A10A5c12588', + CRV_TOKEN: '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978', + }; + + // Impersonate necessary accounts + const accounts = await ethers.getSigners(); + const deployer = accounts[0]; + const dlcAdmin = await ethers.getImpersonatedSigner( + '0xaA2949C5285C2f2887ABD567865344240c29d619' + ); + const dlcCritical = await ethers.getImpersonatedSigner( + '0x24f75096ad315Ab617a3d0f2621aC3e9D391Aa77' + ); + const attestor_1 = await ethers.getImpersonatedSigner( + '0x989E9c4005ABc2a8E4b85544B44d2d95cfDe08de' + ); + const attestor_2 = await ethers.getImpersonatedSigner( + '0xBe4aAE47A62f67bdF93eA9f5F189ae51B1b54492' + ); + const attestor_3 = await ethers.getImpersonatedSigner( + '0x7B254D8C6eBd9662A52180B06920aEA4f23a8940' + ); + const attestor_4 = await ethers.getImpersonatedSigner( + '0x194c697e8343EaB3C53917BA7e597d02687f8BA0' + ); + const attestor_5 = await ethers.getImpersonatedSigner( + '0x2daef70747eb9E97E5f31A9EBDbda593918F8bE7' + ); + const operator = deployer; + const harvester = deployer; + + const attestors = [ + attestor_1, + attestor_2, + attestor_3, + attestor_4, + attestor_5, + ]; + + console.log('\nšŸ’° Funding impersonated accounts...'); + await fundAccount(dlcAdmin.address); + await fundAccount(attestor_1.address); + await fundAccount(attestor_2.address); + await fundAccount(attestor_3.address); + await fundAccount(attestor_4.address); + await fundAccount(attestor_5.address); + + console.log('\nšŸ”„ Upgrading DLCManager...'); + const proxyAdminAddress = await upgrades.erc1967.getAdminAddress( + MAINNET_ADDRESSES.DLC_MANAGER + ); + console.log('Proxy admin address:', proxyAdminAddress); + const connectedProxyAdmin = new ethers.Contract( + proxyAdminAddress, + [ + 'function owner() view returns (address)', + 'function upgrade(address, address) external', + ], + dlcCritical + ); + + const DLCManager = await ethers.getContractFactory('DLCManager'); + const dlcManagerImpl = await DLCManager.deploy(); + await dlcManagerImpl.deployed(); + + // Upgrade using ProxyAdmin + await connectedProxyAdmin.upgrade( + MAINNET_ADDRESSES.DLC_MANAGER, + dlcManagerImpl.address + ); + + console.log('DLCManager upgraded'); + + console.log('\nšŸ“ Getting contract instances...'); + const dlcManager = await ethers.getContractAt( + 'DLCManager', + MAINNET_ADDRESSES.DLC_MANAGER + ); + const dlcBTC = await ethers.getContractAt( + 'DLCBTC', + MAINNET_ADDRESSES.DLC_BTC + ); + + await dlcManager.connect(dlcAdmin).setSkipSignatureVerification(true); + + // Deploy PoolMerchant + console.log('\nšŸ—ļø Deploying PoolMerchant...'); + const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + MAINNET_ADDRESSES.DLC_MANAGER, + MAINNET_ADDRESSES.DLC_BTC, + deployer.address, + ]); + await poolMerchant.deployed(); + console.log('PoolMerchant deployed to:', poolMerchant.address); + + // Whitelisting PoolMerchant + const whitelistTx = await dlcManager + .connect(dlcAdmin) + .whitelistAddress(poolMerchant.address); + await whitelistTx.wait(); + + // Deploy CurveIntegration + console.log('\nšŸ—ļø Deploying CurveIntegration...'); + const CurveIntegration = + await ethers.getContractFactory('CurveIntegration'); + const curveIntegration = await CurveIntegration.deploy( + MAINNET_ADDRESSES.CURVE_POOL, + MAINNET_ADDRESSES.CURVE_GAUGE, + poolMerchant.address, + MAINNET_ADDRESSES.DLC_BTC + ); + await curveIntegration.deployed(); + console.log('CurveIntegration deployed to:', curveIntegration.address); + + // Setup roles + console.log('\nšŸ”‘ Setting up roles...'); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + attestor_1.address + ); + await poolMerchant.grantRole( + await poolMerchant.OPERATOR_ROLE(), + operator.address + ); + await poolMerchant.grantRole( + await poolMerchant.HARVESTER_ROLE(), + harvester.address + ); + + // Add CRV as reward token + console.log('\nšŸŖ™ Adding CRV as reward token...'); + await poolMerchant.addRewardToken(MAINNET_ADDRESSES.CRV_TOKEN); + + // Setup integration + console.log('\nšŸ”„ Setting up CurveIntegration...'); + await poolMerchant.setIntegration(curveIntegration.address, [ + MAINNET_ADDRESSES.CRV_TOKEN, + ]); + + // Create vault + console.log('\nšŸ“¦ Creating vault...'); + const mockBtcTxId = '0x123'; // Mock BTC tx ID + const mockTaprootPubkey = '0x12345'; // Mock taproot pubkey + + const tx = await poolMerchant + .connect(attestor_1) + .createPendingVault(mockTaprootPubkey, mockBtcTxId, { + gasLimit: 1000000, + }); + + const receipt = await tx.wait(); + const vaultId = receipt.events.find( + (e) => e.event === 'PendingVaultCreated' + ).args.uuid; + console.log('Vault created with ID:', vaultId); + + // Fund vault + console.log('\nšŸ’° Funding vault...'); + + const fundAmount = ethers.utils.parseUnits('1', 8); // 1 BTC + console.log('Funding amount:', fundAmount); + + // NOTE: I have added an early return to the multisig checking in the DLCManager + // Because on forked networks its very hard to produce valid signatures + // with the impersonated attestors... so we will skip this part for now + + // const signatureBytesForFunding = await getSignatures( + // { + // uuid: vaultId, + // btcTxId: mockBtcTxId, + // functionString: 'set-status-funded', + // newLockedAmount: fundAmount, + // }, + // attestors, + // 5 + // ); + // console.log('Signatures for funding:', signatureBytesForFunding); + const tx3 = await dlcManager + .connect(attestor_1) + .setStatusFunded(vaultId, mockBtcTxId, [], fundAmount); + await tx3.wait(); + + // Allocate to Curve + console.log('\nšŸ“ˆ Allocating to Curve...'); + await poolMerchant + .connect(operator) + .allocateToIntegration(vaultId, curveIntegration.address); + const shares = await poolMerchant.getVaultShares( + vaultId, + curveIntegration.address + ); + console.log('Allocated shares:', shares.toString()); + + // Wait for some blocks to accrue rewards + console.log('\nā³ Mining blocks to accrue rewards...'); + await hre.network.provider.send('hardhat_mine', ['0x100']); // Mine 256 blocks + + // Harvest rewards + console.log('\nšŸŒ¾ Harvesting rewards...'); + await poolMerchant + .connect(harvester) + .harvestRewards(curveIntegration.address); + const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + curveIntegration.address, + MAINNET_ADDRESSES.CRV_TOKEN + ); + console.log( + 'Pending CRV rewards:', + ethers.utils.formatEther(pendingAmount) + ); + + console.log('\nāœ… Happy path integration test complete!'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/PoolMerchant_integration_test.js b/test/PoolMerchant_integration_test.js deleted file mode 100644 index f78aeac..0000000 --- a/test/PoolMerchant_integration_test.js +++ /dev/null @@ -1,136 +0,0 @@ -const hre = require('hardhat'); -const { ethers, upgrades } = require('hardhat'); - -async function main() { - console.log('\nšŸš€ Starting happy path integration test...'); - - // Configure network and addresses - const MAINNET_ADDRESSES = { - DLC_MANAGER: '0x...', // Add real address - DLC_BTC: '0x...', // Add real address - CURVE_POOL: '0x...', // Add real address - CURVE_GAUGE: '0x...', // Add real address - CRV_TOKEN: '0x...', // Add real address - }; - - // Impersonate necessary accounts - const accounts = await ethers.getSigners(); - const deployer = accounts[0]; - const attestor = await ethers.getImpersonatedSigner('0x...'); // Add real attestor - const operator = await ethers.getImpersonatedSigner('0x...'); // Add real operator - const harvester = await ethers.getImpersonatedSigner('0x...'); // Add real harvester - - console.log('\nšŸ“ Getting contract instances...'); - const dlcManager = await ethers.getContractAt( - 'DLCManager', - MAINNET_ADDRESSES.DLC_MANAGER - ); - const dlcBTC = await ethers.getContractAt( - 'DLCBTC', - MAINNET_ADDRESSES.DLC_BTC - ); - - // Deploy PoolMerchant - console.log('\nšŸ—ļø Deploying PoolMerchant...'); - const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); - const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ - MAINNET_ADDRESSES.DLC_MANAGER, - MAINNET_ADDRESSES.DLC_BTC, - deployer.address, - ]); - await poolMerchant.deployed(); - console.log('PoolMerchant deployed to:', poolMerchant.address); - - // Deploy CurveIntegration - console.log('\nšŸ—ļø Deploying CurveIntegration...'); - const CurveIntegration = - await ethers.getContractFactory('CurveIntegration'); - const curveIntegration = await CurveIntegration.deploy( - MAINNET_ADDRESSES.CURVE_POOL, - MAINNET_ADDRESSES.CURVE_GAUGE, - poolMerchant.address, - MAINNET_ADDRESSES.DLC_BTC - ); - await curveIntegration.deployed(); - console.log('CurveIntegration deployed to:', curveIntegration.address); - - // Setup roles - console.log('\nšŸ”‘ Setting up roles...'); - await poolMerchant.grantRole( - await poolMerchant.ATTESTOR_ROLE(), - attestor.address - ); - await poolMerchant.grantRole( - await poolMerchant.OPERATOR_ROLE(), - operator.address - ); - await poolMerchant.grantRole( - await poolMerchant.HARVESTER_ROLE(), - harvester.address - ); - - // Add CRV as reward token - console.log('\nšŸŖ™ Adding CRV as reward token...'); - await poolMerchant.addRewardToken(MAINNET_ADDRESSES.CRV_TOKEN); - - // Setup integration - console.log('\nšŸ”„ Setting up CurveIntegration...'); - await poolMerchant.setIntegration(curveIntegration.address, [ - MAINNET_ADDRESSES.CRV_TOKEN, - ]); - - // Create vault - console.log('\nšŸ“¦ Creating vault...'); - const tx = await poolMerchant - .connect(attestor) - .createPendingVault('taproot123', 'tx123'); - const receipt = await tx.wait(); - const vaultId = receipt.events.find( - (e) => e.event === 'PendingVaultCreated' - ).args.uuid; - console.log('Vault created with ID:', vaultId); - - // Fund vault (mock) - console.log('\nšŸ’° Funding vault...'); - const fundAmount = ethers.utils.parseUnits('1', 8); // 1 BTC - await dlcManager.connect(attestor).mockFundVault(vaultId, fundAmount); - - // Allocate to Curve - console.log('\nšŸ“ˆ Allocating to Curve...'); - await poolMerchant - .connect(operator) - .allocateToIntegration(vaultId, curveIntegration.address); - const shares = await poolMerchant.getVaultShares( - vaultId, - curveIntegration.address - ); - console.log('Allocated shares:', shares.toString()); - - // Wait for some blocks to accrue rewards - console.log('\nā³ Mining blocks to accrue rewards...'); - await hre.network.provider.send('hardhat_mine', ['0x100']); // Mine 256 blocks - - // Harvest rewards - console.log('\nšŸŒ¾ Harvesting rewards...'); - await poolMerchant - .connect(harvester) - .harvestRewards(curveIntegration.address); - const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - curveIntegration.address, - MAINNET_ADDRESSES.CRV_TOKEN - ); - console.log( - 'Pending CRV rewards:', - ethers.utils.formatEther(pendingAmount) - ); - - console.log('\nāœ… Happy path integration test complete!'); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); From ef97e59eef0a2f8575d519e0db9e4f846fe12c7e Mon Sep 17 00:00:00 2001 From: Rayerleier <1045643889@qq.com> Date: Thu, 31 Oct 2024 17:30:14 +0800 Subject: [PATCH 11/27] fix:decimals and interfaces --- contracts/integrations/curve.sol | 39 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/contracts/integrations/curve.sol b/contracts/integrations/curve.sol index 64a4d54..5095cc6 100644 --- a/contracts/integrations/curve.sol +++ b/contracts/integrations/curve.sol @@ -22,14 +22,26 @@ interface ICurvePool { function coins(uint256 i) external view returns (address); } -// NOTE: https://curve.readthedocs.io/dao-gauges.html#liquiditygaugev3 -// @Rayerleier: I am not fully sure all of these will work. Let's try and test tomorrow. +// NOTE: I've altered deposit态withdraw态claim_rewards make up to date. interface ICurveGauge { - function deposit(uint256 _value, address _user) external; - function withdraw(uint256 _value, bool _claim_rewards) external; - function claim_rewards(address _addr) external; + function deposit( + uint256 _value, + address _user, + bool _claim_rewards + ) external; + + function withdraw( + uint256 _value, + address _user, + bool _claim_rewards + ) external; + + function claim_rewards(address _addr, address _receiver) external; + function reward_tokens(uint256 i) external view returns (address); + function reward_count() external view returns (uint256); + function claimable_reward( address _user, address _reward_token @@ -70,6 +82,7 @@ contract CurveIntegration is IIntegration, Ownable { // NOTE: we might have to change how shares/amounts translate... // since dlcBTC is 8 decimals and the curve pool is 18 decimals + // NOTE: decimals changed function deposit( uint256 amount ) external override onlyOwner returns (uint256 shares) { @@ -87,7 +100,8 @@ contract CurveIntegration is IIntegration, Ownable { // Approve and deposit LP tokens into gauge IERC20(curvePoolAddress).approve(curveGaugeAddress, shares); - curveGauge.deposit(shares, address(this)); + curveGauge.deposit(shares, address(this), false); + shares = _fromPoolDecimals(shares); return shares; } @@ -95,8 +109,9 @@ contract CurveIntegration is IIntegration, Ownable { function withdraw( uint256 shares ) external override onlyOwner returns (uint256 amount) { + shares = _toPoolDecimals(shares); // Step 1: Withdraw LP tokens from gauge - curveGauge.withdraw(shares, true); // Claim rewards during withdrawal + curveGauge.withdraw(shares, address(this), true); // Claim rewards during withdrawal // Step 2: Remove liquidity for dlcBTC amount = curvePool.remove_liquidity_one_coin( @@ -115,7 +130,7 @@ contract CurveIntegration is IIntegration, Ownable { onlyOwner returns (uint256[] memory amounts) { - curveGauge.claim_rewards(address(this)); + curveGauge.claim_rewards(address(this), msg.sender); uint256 rewardCount = curveGauge.reward_count(); amounts = new uint256[](rewardCount); @@ -160,4 +175,12 @@ contract CurveIntegration is IIntegration, Ownable { ); } } + + function _toPoolDecimals(uint256 amount) internal pure returns (uint256) { + return amount * 1e10; + } + + function _fromPoolDecimals(uint256 amount) internal pure returns (uint256) { + return amount / 1e10; + } } From 375feb04be55a808475950d45d7899ac1a5d3495 Mon Sep 17 00:00:00 2001 From: Rayerleier <1045643889@qq.com> Date: Thu, 31 Oct 2024 21:00:09 +0800 Subject: [PATCH 12/27] adding: enzyme.sol --- contracts/integrations/enzyme.sol | 333 +++++++++++------------------- 1 file changed, 119 insertions(+), 214 deletions(-) diff --git a/contracts/integrations/enzyme.sol b/contracts/integrations/enzyme.sol index e63975e..0a00f07 100644 --- a/contracts/integrations/enzyme.sol +++ b/contracts/integrations/enzyme.sol @@ -1,214 +1,119 @@ -// // SPDX-License-Identifier: MIT -// // ___ __ ___ __ _ _ -// // / \/ / / __\ / /(_)_ __ | | __ -// // / /\ / / / / / / | | '_ \| |/ / -// // / /_// /__/ /____/ /__| | | | | < -// // /___,'\____|____(_)____/_|_| |_|_|\_\ - -// pragma solidity 0.8.18; - -// import "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol"; -// import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -// import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -// import "@openzeppelin/contracts/utils/Strings.sol"; -// import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -// import "./DLCLinkLibrary.sol"; -// import "./DLCManager.sol"; - -// // interface IEnzymeVault { -// // function buyShares( -// // uint256 _investmentAmount, -// // uint256 _minSharesQuantity -// // ) external returns (uint256); -// // } - -// interface IGlobalConfigLibComptrollerV4 { -// function buyShares( -// uint256 _investmentAmount, -// uint256 _minSharesQuantity -// ) external returns (uint256 sharesReceived_); - -// function getDenominationAsset() -// external -// view -// returns (address denominationAsset_); - -// function redeemSharesForSpecificAssets( -// address _recipient, -// uint256 _sharesQuantity, -// address[] calldata _payoutAssets, -// uint256[] calldata _payoutAssetPercentages -// ) external returns (uint256[] memory payoutAmounts_); - -// function redeemSharesInKind( -// address _recipient, -// uint256 _sharesQuantity, -// address[] calldata _additionalAssets, -// address[] calldata _assetsToSkip -// ) -// external -// returns ( -// address[] memory payoutAssets_, -// uint256[] memory payoutAmounts_ -// ); -// } - -// contract PoolMerchant is -// Initializable, -// AccessControlDefaultAdminRulesUpgradeable, -// PausableUpgradeable -// { -// using DLCLink for DLCLink.DLC; -// using Strings for string; -// using SafeERC20 for IERC20; - -// bytes32 public constant DLC_ADMIN_ROLE = -// 0x2bf88000669ee6f7a648a231f4adbc117f5a8e34f980c08420b9b9a9f2640aa1; // keccak256("DLC_ADMIN_ROLE") - -// IGlobalConfigLibComptrollerV4 public enzymeVault; -// DLCManager public dlcManager; -// IERC20 public dlcBTC; -// uint256 private _nonce; -// mapping(bytes32 => string) public uuidToTaprootPubkey; -// mapping(bytes32 => address) public uuidToUserAddress; -// mapping(bytes32 => uint256) public sweptAmounts; - -// function initialize( -// address defaultAdmin, -// address dlcAdminRole, -// address dlcManagerContract, -// address dlcBTCContract, -// address enzymeVaultContract -// ) public initializer { -// __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); -// _grantRole(DLC_ADMIN_ROLE, dlcAdminRole); -// dlcManager = DLCManager(dlcManagerContract); -// dlcBTC = IERC20(dlcBTCContract); -// enzymeVault = IGlobalConfigLibComptrollerV4(enzymeVaultContract); -// _nonce = 0; -// } - -// /// @custom:oz-upgrades-unsafe-allow constructor -// constructor() { -// _disableInitializers(); -// } - -// error VaultAlreadyExists(bytes32 uuid); - -// function getNewUUID(address userAddress) public view returns (bytes32) { -// return dlcManager.generateUUID(userAddress, block.timestamp); -// } - -// // TODO: auth -// // called by attestors -// function createPendingVault( -// address userAddress, -// bytes32 uuid, -// string memory taprootPubkey, -// string calldata wdTxId -// ) public { -// if (uuidToTaprootPubkey[uuid].equal(taprootPubkey)) { -// revert VaultAlreadyExists(uuid); -// } -// uuidToTaprootPubkey[uuid] = taprootPubkey; -// uuidToUserAddress[uuid] = userAddress; -// dlcManager.setupPendingVault(uuid, taprootPubkey, wdTxId); -// } - -// function getSharesForUUID(bytes32 uuid) public view returns (uint256) { -// DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); -// uint256 shares = _dlc.valueMinted; -// return shares; -// } - -// function getSweptAmountForUUID(bytes32 uuid) public view returns (uint256) { -// return sweptAmounts[uuid]; -// } - -// // called by attestors -// function sweepDeposit() public { -// DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( -// address(this) -// ); - -// for (uint256 i = 0; i < _allDLCs.length; i++) { -// bytes32 uuid = _allDLCs[i].uuid; -// uint256 currentValueMinted = _allDLCs[i].valueMinted; -// uint256 sweptAmount = sweptAmounts[uuid]; - -// if (currentValueMinted > sweptAmount) { -// uint256 difference = currentValueMinted - sweptAmount; - -// // Approve the Enzyme vault to spend dlcBTC tokens -// dlcBTC.approve(address(enzymeVault), difference); - -// enzymeVault.buyShares(difference, 1); - -// sweptAmounts[uuid] = currentValueMinted; // Update the local tracker -// } -// } -// } - -// // attestors call this -// function withdraw( -// bytes32 uuid, -// uint256 amount, -// string memory taprootPubkey, -// string calldata /*wdTxId*/ -// ) public { -// require( -// keccak256(abi.encodePacked(uuidToTaprootPubkey[uuid])) == -// keccak256(abi.encodePacked(taprootPubkey)), -// "Invalid taproot pubkey" -// ); - -// address[] memory payoutAssets = new address[](1); -// payoutAssets[0] = address(dlcBTC); -// uint256[] memory payoutPercentage = new uint256[](1); -// payoutPercentage[0] = 10000; - -// enzymeVault.redeemSharesForSpecificAssets( -// address(this), -// amount, -// payoutAssets, -// payoutPercentage -// ); - -// dlcManager.withdraw(uuid, amount); -// DLCLink.DLC memory _dlc = dlcManager.getDLC(uuid); -// sweptAmounts[uuid] = _dlc.valueMinted; // Update the local tracker -// } - -// // function sweepRedeem() public { -// // DLCLink.DLC[] memory _allDLCs = dlcManager.getAllVaultsForAddress( -// // address(this) -// // ); - -// // for (uint256 i = 0; i < _allDLCs.length; i++) { -// // bytes32 uuid = _allDLCs[i].uuid; -// // uint256 currentValueMinted = _allDLCs[i].valueMinted; -// // uint256 sweptAmount = sweptAmounts[uuid]; - -// // if (currentValueMinted < sweptAmount) { -// // uint256 difference = sweptAmount - currentValueMinted; - -// // // Approve the Enzyme vault to spend dlcBTC tokens -// // // dlcBTC.approve(address(enzymeVault), difference); - -// // address[] memory payoutAssets = new address[](1); -// // payoutAssets[0] = address(dlcBTC); -// // uint256[] memory payoutPercentage = new uint256[](1); -// // payoutPercentage[0] = 100; - -// // enzymeVault.redeemSharesForSpecificAssets( -// // address(this), -// // difference, -// // payoutAssets, -// // payoutPercentage -// // ); - -// // sweptAmounts[uuid] = currentValueMinted; // Update the local tracker -// // } -// // } -// // } -// } +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "../interfaces/IIntegration.sol"; + +interface IGlobalConfigLibComptrollerV4 { + function buyShares( + uint256 _investmentAmount, + uint256 _minSharesQuantity + ) external returns (uint256 sharesReceived_); + + function getDenominationAsset() + external + view + returns (address denominationAsset_); + + function redeemSharesForSpecificAssets( + address _recipient, + uint256 _sharesQuantity, + address[] calldata _payoutAssets, + uint256[] calldata _payoutAssetPercentages + ) external returns (uint256[] memory payoutAmounts_); +} + +contract EnzymeIntegration is IIntegration, Ownable { + address public enzymeVaultAddress; + IGlobalConfigLibComptrollerV4 public enzymeVault; + address public dlcBTC; + + constructor( + address _enzymeVaultAddress, + address _poolMerchant, + address _dlcBTC + ) Ownable() { + enzymeVaultAddress = _enzymeVaultAddress; + enzymeVault = IGlobalConfigLibComptrollerV4(enzymeVaultAddress); + dlcBTC = _dlcBTC; + + // Verify that vault's denomination asset is dlcBTC + require( + enzymeVault.getDenominationAsset() == dlcBTC, + "Vault denomination asset must be dlcBTC" + ); + + transferOwnership(_poolMerchant); + } + + function deposit( + uint256 amount + ) external override onlyOwner returns (uint256 shares) { + // The dlcBTC tokens are still in the PoolMerchant, which has approved us to spend them + + // Transfer the dlcBTC from PoolMerchant and approve Enzyme vault to spend it + IERC20(dlcBTC).transferFrom(msg.sender, address(this), amount); + IERC20(dlcBTC).approve(enzymeVaultAddress, amount); + + // Buy shares in the Enzyme vault + shares = enzymeVault.buyShares(amount, 1); // minimum 1 share + + return shares; + } + + function withdraw( + uint256 shares + ) external override onlyOwner returns (uint256 amount) { + // Setup redemption parameters for dlcBTC only + address[] memory payoutAssets = new address[](1); + payoutAssets[0] = dlcBTC; + + uint256[] memory payoutPercentages = new uint256[](1); + payoutPercentages[0] = 10000; // 100% in basis points + + // Redeem shares for dlcBTC + uint256[] memory amounts = enzymeVault.redeemSharesForSpecificAssets( + msg.sender, // Send directly to PoolMerchant + shares, + payoutAssets, + payoutPercentages + ); + + return amounts[0]; + } + + // Since Enzyme doesn't have a direct reward claiming mechanism like Curve, + // we'll implement empty reward functions to maintain interface compatibility + + function claimRewards() + external + view + override + onlyOwner + returns (uint256[] memory amounts) + { + amounts = new uint256[](0); + return amounts; + } + + function getRewardTokens() + external + pure + override + returns (address[] memory tokens) + { + tokens = new address[](0); + return tokens; + } + + function getPendingRewards() + external + pure + override + returns (uint256[] memory amounts) + { + amounts = new uint256[](0); + return amounts; + } +} From 3650380410f64b46feec42682415b2119711fba6 Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 31 Oct 2024 18:01:36 +0100 Subject: [PATCH 13/27] feat: single integration vaults --- contracts/PoolMerchant.sol | 394 ++++++++++++++++------------------ scripts/pool_merchant_flow.js | 164 ++++++++------ test/PoolMerchant.test.js | 318 ++++++++++----------------- 3 files changed, 399 insertions(+), 477 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 8bf0d8f..136652e 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -60,13 +60,15 @@ contract PoolMerchant is struct UserReward { uint256 lastClaimedAt; - uint256 pendingAmount; // For ERC20s + uint256 pendingAmount; } struct VaultInfo { - mapping(address => uint256) integrationShares; // integration -> shares - mapping(address => mapping(address => UserReward)) rewards; // integration -> token -> reward - uint256 totalAllocated; // Track total amount allocated to integrations + address integration; + uint256 shares; + uint256 allocated; + mapping(address => UserReward) rewards; // token -> reward + uint256 integrationIndex; // Index in the integration's vault array + 1 (0 means not in array) } struct Integration { @@ -74,14 +76,13 @@ contract PoolMerchant is bool isActive; uint256 totalShares; address[] supportedRewardTokens; + bytes32[] vaults; // Array of vault UUIDs using this integration } mapping(bytes32 => VaultInfo) internal _vaults; mapping(address => Integration) public integrations; mapping(address => RewardToken) public rewardTokens; address[] public activeIntegrations; - mapping(address => bytes32[]) private _integrationVaults; // integration -> array of vault IDs - mapping(address => mapping(bytes32 => uint256)) private _vaultIndices; // integration -> vault -> index in array uint256 public totalValueLocked; uint256[50] private __gap; @@ -93,7 +94,8 @@ contract PoolMerchant is event PendingVaultCreated( bytes32 indexed uuid, string taprootPubKey, - string withdrawalTxId + string withdrawalTxId, + address integration ); event VaultWithdrawn(bytes32 indexed uuid, uint256 amount); event IntegrationAdded( @@ -114,7 +116,6 @@ contract PoolMerchant is ); event RewardsClaimed( bytes32 indexed uuid, - address indexed integration, address indexed rewardToken, uint256 amount ); @@ -148,7 +149,8 @@ contract PoolMerchant is function createPendingVault( string calldata taprootPubKey, - string calldata withdrawalTxId + string calldata withdrawalTxId, + address integration ) external onlyRole(ATTESTOR_ROLE) @@ -156,16 +158,22 @@ contract PoolMerchant is whenNotPaused returns (bytes32) { - // Create pending vault in DLCManager + require(integrations[integration].isActive, "Integration not active"); + bytes32 _uuid = dlcManager.setupPendingVault( taprootPubKey, withdrawalTxId ); - // Initialize our tracking (no need to store DLC data) - _vaults[_uuid].integrationShares[address(0)] = 0; // Just initialize the mapping + _vaults[_uuid].integration = integration; + _addVaultToIntegration(_uuid, integration); - emit PendingVaultCreated(_uuid, taprootPubKey, withdrawalTxId); + emit PendingVaultCreated( + _uuid, + taprootPubKey, + withdrawalTxId, + integration + ); return _uuid; } @@ -176,26 +184,68 @@ contract PoolMerchant is DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.uuid != bytes32(0), "Vault does not exist"); - // First withdraw from any active integrations - _withdrawFromIntegrations(dlc.uuid, amount); + VaultInfo storage vault = _vaults[uuid]; + address integration = vault.integration; + require(integration != address(0), "No integration set"); + require(amount <= vault.allocated, "Amount exceeds allocation"); + + Integration storage integ = integrations[integration]; + + // Harvest any pending rewards + if (vault.shares > 0) { + _harvestRewardsForVault(uuid); + } + + // Calculate shares to withdraw based on requested amount + uint256 sharesToWithdraw; + if (amount == vault.allocated) { + // If withdrawing all, withdraw all shares + sharesToWithdraw = vault.shares; + } else { + // Otherwise, withdraw proportional shares + sharesToWithdraw = (vault.shares * amount) / vault.allocated; + } + + // Withdraw from integration + uint256 received = integ.strategy.withdraw(sharesToWithdraw); + + // Update state based on what we actually received + vault.shares -= sharesToWithdraw; + integrations[integration].totalShares -= sharesToWithdraw; + + // If we received less than requested, we need to adjust the withdrawal amount + uint256 withdrawAmount; + if (received < amount) { + // We can only withdraw what we actually received + withdrawAmount = received; + // Adjust allocation down based on what we actually received + vault.allocated -= received; + } else { + // We got enough or more than requested + withdrawAmount = amount; + vault.allocated -= amount; + if (received > amount) { + // If we got extra, add it to allocation + vault.allocated += (received - amount); + } + } - // Then withdraw from DLCManager - dlcManager.withdraw(dlc.uuid, amount); + // Clean up if fully withdrawn + if (vault.shares == 0) { + _removeVaultFromIntegration(uuid, integration); + } - emit VaultWithdrawn(dlc.uuid, amount); + // Withdraw from DLCManager with adjusted amount + dlcManager.withdraw(uuid, withdrawAmount); + emit VaultWithdrawn(uuid, withdrawAmount); } - // Harvesting rewards explained: - // 1. External protocols (like Enzyme, YieldNest) generate rewards - // 2. A harvester (automated or manual) collects these rewards - // 3. The harvester calls this function to distribute rewards to vault holders - function harvestRewards( - address integration - ) external onlyRole(HARVESTER_ROLE) nonReentrant whenNotPaused { - require(integrations[integration].isActive, "Integration not active"); + function _harvestRewardsForVault(bytes32 uuid) internal { + VaultInfo storage vault = _vaults[uuid]; + address integration = vault.integration; Integration storage integ = integrations[integration]; - // Claim rewards from integration + // Get and claim rewards from integration uint256[] memory amounts = integ.strategy.claimRewards(); address[] memory rewardAddresses = integ.strategy.getRewardTokens(); require( @@ -203,59 +253,44 @@ contract PoolMerchant is "Invalid reward data" ); - uint256 totalShares = integ.totalShares; - bytes32[] memory activeVaults = _getActiveVaultsForIntegration( - integration - ); - - // Distribute each reward token + // Process each reward token for (uint256 i = 0; i < amounts.length; i++) { address rewardAddress = rewardAddresses[i]; uint256 amount = amounts[i]; - // Skip unsupported reward tokens instead of reverting if (!rewardTokens[rewardAddress].isActive) { continue; } - // Only distribute and emit events for supported tokens - for (uint256 j = 0; j < activeVaults.length; j++) { - bytes32 uuid = activeVaults[j]; - uint256 vaultShares = _vaults[uuid].integrationShares[ - integration - ]; - - if (vaultShares > 0) { - UserReward storage reward = _vaults[uuid].rewards[ - integration - ][rewardAddress]; - uint256 vaultReward = (amount * vaultShares) / totalShares; - reward.pendingAmount += vaultReward; - reward.lastClaimedAt = block.timestamp; - } + // Calculate this vault's share of the rewards + if (amount > 0 && vault.shares > 0) { + UserReward storage reward = vault.rewards[rewardAddress]; + uint256 vaultReward = (amount * vault.shares) / + integ.totalShares; + reward.pendingAmount += vaultReward; + reward.lastClaimedAt = block.timestamp; + + emit RewardsHarvested( + integration, + rewardAddress, + address(this), + vaultReward + ); } - - emit RewardsHarvested( - integration, - rewardAddress, - msg.sender, - amount - ); } } - function getPendingIntegrationRewards( + function harvestRewardsForIntegration( address integration - ) - external - view - returns (address[] memory tokens, uint256[] memory amounts) - { + ) external onlyRole(HARVESTER_ROLE) nonReentrant whenNotPaused { require(integrations[integration].isActive, "Integration not active"); - Integration storage integ = integrations[integration]; - tokens = integ.strategy.getRewardTokens(); - amounts = integ.strategy.getPendingRewards(); + bytes32[] memory activeVaults = _getVaultsForIntegration(integration); + for (uint256 i = 0; i < activeVaults.length; i++) { + if (_vaults[activeVaults[i]].shares > 0) { + _harvestRewardsForVault(activeVaults[i]); + } + } } // Claim rewards (ERC20s only) @@ -263,12 +298,10 @@ contract PoolMerchant is // So, it would not be msg.sender who gets this function claimRewards( bytes32 uuid, - address integration, address rewardToken ) external nonReentrant whenNotPaused { - UserReward storage reward = _vaults[uuid].rewards[integration][ - rewardToken - ]; + VaultInfo storage vault = _vaults[uuid]; + UserReward storage reward = vault.rewards[rewardToken]; require(reward.pendingAmount > 0, "No rewards to claim"); uint256 amount = reward.pendingAmount; @@ -279,7 +312,7 @@ contract PoolMerchant is "Reward transfer failed" ); - emit RewardsClaimed(uuid, integration, rewardToken, amount); + emit RewardsClaimed(uuid, rewardToken, amount); } //////////////////////////////////////////////////////////////// @@ -302,7 +335,8 @@ contract PoolMerchant is strategy: IIntegration(integration), isActive: true, totalShares: integrations[integration].totalShares, // Preserve existing shares if any - supportedRewardTokens: supportedRewards + supportedRewardTokens: supportedRewards, + vaults: _getVaultsForIntegration(integration) }); // Update active integrations list if new @@ -313,210 +347,146 @@ contract PoolMerchant is emit IntegrationAdded(integration, supportedRewards); } - // Allocate dlcBTC to an integration + // Allocate dlcBTC to the vault's integration function allocateToIntegration( - bytes32 uuid, - address integration + bytes32 uuid ) external onlyRole(OPERATOR_ROLE) nonReentrant whenNotPaused { + VaultInfo storage vault = _vaults[uuid]; + address integration = vault.integration; + require(integration != address(0), "No integration set"); require(integrations[integration].isActive, "Integration not active"); - // Get current vault state DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.valueMinted > 0, "Vault not funded"); - // Calculate amount available to allocate - uint256 unallocated = dlc.valueMinted - _vaults[uuid].totalAllocated; + uint256 unallocated = dlc.valueMinted - vault.allocated; require(unallocated > 0, "Nothing to allocate"); - // First approve the integration to spend dlcBTC require(dlcBTC.approve(integration, unallocated), "Approval failed"); - // Then deposit through the integration uint256 shares = integrations[integration].strategy.deposit( unallocated ); - // Update share accounting - _vaults[uuid].integrationShares[integration] += shares; - _vaults[uuid].totalAllocated += unallocated; + vault.shares += shares; + vault.allocated += unallocated; integrations[integration].totalShares += shares; - _addVaultToIntegration(integration, uuid); - emit SharesAllocated(uuid, integration, shares); } - function _isInActiveIntegrations( + function _addVaultToIntegration( + bytes32 uuid, address integration - ) internal view returns (bool) { - for (uint256 i = 0; i < activeIntegrations.length; i++) { - if (activeIntegrations[i] == integration) { - return true; - } + ) internal { + VaultInfo storage vault = _vaults[uuid]; + if (vault.integrationIndex == 0) { + integrations[integration].vaults.push(uuid); + vault.integrationIndex = integrations[integration].vaults.length; } - return false; } - function _withdrawFromIntegrations( + function _removeVaultFromIntegration( bytes32 uuid, - uint256 totalAmount + address integration ) internal { - uint256 remainingAmount = totalAmount; - - for ( - uint256 i = 0; - i < activeIntegrations.length && remainingAmount > 0; - i++ - ) { - address integration = activeIntegrations[i]; - uint256 shareAmount = _vaults[uuid].integrationShares[integration]; - - if (shareAmount > 0) { - Integration storage integ = integrations[integration]; - uint256 withdrawAmount = (shareAmount * totalAmount) / - integ.totalShares; + VaultInfo storage vault = _vaults[uuid]; + uint256 index = vault.integrationIndex; + + if (index > 0) { + index--; // Convert from 1-based to 0-based - if (withdrawAmount > 0) { - uint256 received = integ.strategy.withdraw(withdrawAmount); - remainingAmount -= received; - _vaults[uuid].integrationShares[ - integration - ] -= withdrawAmount; - _vaults[uuid].totalAllocated -= received; // Update allocated tracking - integ.totalShares -= withdrawAmount; - - if (_vaults[uuid].integrationShares[integration] == 0) { - _removeVaultFromIntegration(integration, uuid); - } - } + // Get the array of vaults for this integration + bytes32[] storage vaults = integrations[integration].vaults; + + // If this isn't the last element, move the last element to this position + if (index != vaults.length - 1) { + bytes32 lastVault = vaults[vaults.length - 1]; + vaults[index] = lastVault; + _vaults[lastVault].integrationIndex = index + 1; // Update to 1-based index } - } - require(remainingAmount == 0, "Insufficient liquidity in integrations"); + // Remove the last element + vaults.pop(); + vault.integrationIndex = 0; + } } - // Helper function to get active _vaults for an integration - function _getActiveVaultsForIntegration( + function _isInActiveIntegrations( address integration - ) internal view returns (bytes32[] memory) { - bytes32[] memory allVaults = _integrationVaults[integration]; - uint256 activeCount = 0; - - // First count active vaults - for (uint256 i = 0; i < allVaults.length; i++) { - if (_vaults[allVaults[i]].integrationShares[integration] > 0) { - activeCount++; + ) internal view returns (bool) { + for (uint256 i = 0; i < activeIntegrations.length; i++) { + if (activeIntegrations[i] == integration) { + return true; } } + return false; + } - // Create result array with exact size - bytes32[] memory activeVaults = new bytes32[](activeCount); - uint256 currentIndex = 0; - - // Fill result array - for ( - uint256 i = 0; - i < allVaults.length && currentIndex < activeCount; - i++ - ) { - if (_vaults[allVaults[i]].integrationShares[integration] > 0) { - activeVaults[currentIndex] = allVaults[i]; - currentIndex++; - } - } + function getIntegrationVaults( + address integration + ) external view returns (bytes32[] memory) { + return integrations[integration].vaults; + } - return activeVaults; + function _getVaultsForIntegration( + address integration + ) internal view returns (bytes32[] memory) { + return integrations[integration].vaults; } //////////////////////////////////////////////////////////////// // VAULT FUNCTIONS // //////////////////////////////////////////////////////////////// - function getVaultShares( - bytes32 uuid, - address integration - ) external view returns (uint256) { - return _vaults[uuid].integrationShares[integration]; + function getVaultShares(bytes32 uuid) external view returns (uint256) { + return _vaults[uuid].shares; + } + + function getVaultIntegration(bytes32 uuid) external view returns (address) { + return _vaults[uuid].integration; } function getVaultReward( bytes32 uuid, - address integration, address rewardToken ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { - UserReward storage reward = _vaults[uuid].rewards[integration][ - rewardToken - ]; + UserReward storage reward = _vaults[uuid].rewards[rewardToken]; return (reward.lastClaimedAt, reward.pendingAmount); } - // Helper function to get total shares in an integration for a vault - function getVaultTotalShares( - bytes32 uuid - ) external view returns (uint256 totalShares) { - for (uint256 i = 0; i < activeIntegrations.length; i++) { - totalShares += _vaults[uuid].integrationShares[ - activeIntegrations[i] - ]; - } - } - - function getUnallocatedAmount(bytes32 uuid) public view returns (uint256) { - DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); - return dlc.valueMinted - _vaults[uuid].totalAllocated; - } - function getVaultAllocationDetails( bytes32 uuid ) external view - returns ( - uint256 totalMinted, - uint256 totalAllocated, - uint256 unallocated - ) + returns (uint256 valueMinted, uint256 allocated, uint256 unallocated) { DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); - totalMinted = dlc.valueMinted; - totalAllocated = _vaults[uuid].totalAllocated; - unallocated = totalMinted - totalAllocated; + VaultInfo storage vault = _vaults[uuid]; + + valueMinted = dlc.valueMinted; + allocated = vault.allocated; + unallocated = valueMinted - allocated; } - function _addVaultToIntegration( - address integration, - bytes32 uuid - ) internal { - if (_vaultIndices[integration][uuid] == 0) { - // 0 means not found - _integrationVaults[integration].push(uuid); - _vaultIndices[integration][uuid] = _integrationVaults[integration] - .length; - } + function getUnallocatedAmount(bytes32 uuid) public view returns (uint256) { + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); + return dlc.valueMinted - _vaults[uuid].allocated; } - function _removeVaultFromIntegration( - address integration, - bytes32 uuid - ) internal { - uint256 index = _vaultIndices[integration][uuid]; - if (index > 0) { - // If vault exists in array - index--; // Convert from 1-based to 0-based index - - // Get the last element - uint256 lastIndex = _integrationVaults[integration].length - 1; - if (index != lastIndex) { - // Move last element to the removed element's position - bytes32 lastVault = _integrationVaults[integration][lastIndex]; - _integrationVaults[integration][index] = lastVault; - _vaultIndices[integration][lastVault] = index + 1; // Update to 1-based index - } + function getPendingIntegrationRewards( + address integration + ) + external + view + returns (address[] memory tokens, uint256[] memory amounts) + { + require(integrations[integration].isActive, "Integration not active"); + Integration storage integ = integrations[integration]; - // Remove last element - _integrationVaults[integration].pop(); - delete _vaultIndices[integration][uuid]; - } + tokens = integ.strategy.getRewardTokens(); + amounts = integ.strategy.getPendingRewards(); } //////////////////////////////////////////////////////////////// diff --git a/scripts/pool_merchant_flow.js b/scripts/pool_merchant_flow.js index 36f0358..5fabc9b 100644 --- a/scripts/pool_merchant_flow.js +++ b/scripts/pool_merchant_flow.js @@ -62,13 +62,11 @@ async function main() { ]; console.log('\nšŸ’° Funding impersonated accounts...'); - await fundAccount(dlcAdmin.address); - await fundAccount(attestor_1.address); - await fundAccount(attestor_2.address); - await fundAccount(attestor_3.address); - await fundAccount(attestor_4.address); - await fundAccount(attestor_5.address); + for (const account of [dlcAdmin, ...attestors]) { + await fundAccount(account.address); + } + // DLCManager upgrade remains the same console.log('\nšŸ”„ Upgrading DLCManager...'); const proxyAdminAddress = await upgrades.erc1967.getAdminAddress( MAINNET_ADDRESSES.DLC_MANAGER @@ -87,7 +85,6 @@ async function main() { const dlcManagerImpl = await DLCManager.deploy(); await dlcManagerImpl.deployed(); - // Upgrade using ProxyAdmin await connectedProxyAdmin.upgrade( MAINNET_ADDRESSES.DLC_MANAGER, dlcManagerImpl.address @@ -118,11 +115,8 @@ async function main() { await poolMerchant.deployed(); console.log('PoolMerchant deployed to:', poolMerchant.address); - // Whitelisting PoolMerchant - const whitelistTx = await dlcManager - .connect(dlcAdmin) - .whitelistAddress(poolMerchant.address); - await whitelistTx.wait(); + // Whitelist PoolMerchant + await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); // Deploy CurveIntegration console.log('\nšŸ—ļø Deploying CurveIntegration...'); @@ -137,8 +131,8 @@ async function main() { await curveIntegration.deployed(); console.log('CurveIntegration deployed to:', curveIntegration.address); - // Setup roles - console.log('\nšŸ”‘ Setting up roles...'); + // Setup roles and integration + console.log('\nšŸ”‘ Setting up roles and integration...'); await poolMerchant.grantRole( await poolMerchant.ATTESTOR_ROLE(), attestor_1.address @@ -152,26 +146,29 @@ async function main() { harvester.address ); - // Add CRV as reward token console.log('\nšŸŖ™ Adding CRV as reward token...'); await poolMerchant.addRewardToken(MAINNET_ADDRESSES.CRV_TOKEN); - // Setup integration console.log('\nšŸ”„ Setting up CurveIntegration...'); await poolMerchant.setIntegration(curveIntegration.address, [ MAINNET_ADDRESSES.CRV_TOKEN, ]); - // Create vault - console.log('\nšŸ“¦ Creating vault...'); - const mockBtcTxId = '0x123'; // Mock BTC tx ID - const mockTaprootPubkey = '0x12345'; // Mock taproot pubkey + // Create vault with integration + console.log('\nšŸ“¦ Creating vault with Curve integration...'); + const mockBtcTxId = '0x123'; + const mockTaprootPubkey = '0x12345'; const tx = await poolMerchant .connect(attestor_1) - .createPendingVault(mockTaprootPubkey, mockBtcTxId, { - gasLimit: 1000000, - }); + .createPendingVault( + mockTaprootPubkey, + mockBtcTxId, + curveIntegration.address, + { + gasLimit: 1000000, + } + ); const receipt = await tx.wait(); const vaultId = receipt.events.find( @@ -181,25 +178,9 @@ async function main() { // Fund vault console.log('\nšŸ’° Funding vault...'); - const fundAmount = ethers.utils.parseUnits('1', 8); // 1 BTC - console.log('Funding amount:', fundAmount); - - // NOTE: I have added an early return to the multisig checking in the DLCManager - // Because on forked networks its very hard to produce valid signatures - // with the impersonated attestors... so we will skip this part for now - - // const signatureBytesForFunding = await getSignatures( - // { - // uuid: vaultId, - // btcTxId: mockBtcTxId, - // functionString: 'set-status-funded', - // newLockedAmount: fundAmount, - // }, - // attestors, - // 5 - // ); - // console.log('Signatures for funding:', signatureBytesForFunding); + console.log('Funding amount:', fundAmount.toString()); + const tx3 = await dlcManager .connect(attestor_1) .setStatusFunded(vaultId, mockBtcTxId, [], fundAmount); @@ -207,35 +188,92 @@ async function main() { // Allocate to Curve console.log('\nšŸ“ˆ Allocating to Curve...'); - await poolMerchant - .connect(operator) - .allocateToIntegration(vaultId, curveIntegration.address); - const shares = await poolMerchant.getVaultShares( - vaultId, - curveIntegration.address - ); + await poolMerchant.connect(operator).allocateToIntegration(vaultId); + + const shares = await poolMerchant.getVaultShares(vaultId); console.log('Allocated shares:', shares.toString()); - // Wait for some blocks to accrue rewards + // Mine blocks and check initial state console.log('\nā³ Mining blocks to accrue rewards...'); await hre.network.provider.send('hardhat_mine', ['0x100']); // Mine 256 blocks - // Harvest rewards - console.log('\nšŸŒ¾ Harvesting rewards...'); - await poolMerchant - .connect(harvester) - .harvestRewards(curveIntegration.address); - const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - curveIntegration.address, - MAINNET_ADDRESSES.CRV_TOKEN - ); + // Perform partial withdrawal to trigger reward harvest console.log( - 'Pending CRV rewards:', - ethers.utils.formatEther(pendingAmount) + '\nšŸ¦ Performing partial withdrawal to trigger reward harvest...' ); - - console.log('\nāœ… Happy path integration test complete!'); + const withdrawAmount = fundAmount.div(2); + + console.log('\nšŸ” Testing withdrawal process...'); + + // Step 1: Check initial state + const vaultBefore = await poolMerchant.getVaultAllocationDetails(vaultId); + const sharesBefore = await poolMerchant.getVaultShares(vaultId); + console.log('\nInitial state:'); + console.log(' - Total minted:', vaultBefore.valueMinted.toString()); + console.log(' - Allocated:', vaultBefore.allocated.toString()); + console.log(' - Shares:', sharesBefore.toString()); + + // Step 2: Perform withdrawal + console.log('\nAttempting withdrawal of:', withdrawAmount.toString()); + + try { + // First just try the withdrawal + const withdrawTx = await poolMerchant + .connect(attestor_1) + .withdrawFromVault(vaultId, withdrawAmount, { + gasLimit: 2000000, + }); + + await withdrawTx.wait(); + console.log('Withdrawal successful!'); + + // Check post-withdrawal state + const vaultAfter = + await poolMerchant.getVaultAllocationDetails(vaultId); + const sharesAfter = await poolMerchant.getVaultShares(vaultId); + console.log('\nPost-withdrawal state:'); + console.log(' - Total minted:', vaultAfter.valueMinted.toString()); + console.log(' - Allocated:', vaultAfter.allocated.toString()); + console.log(' - Shares:', sharesAfter.toString()); + + // Step 3: Check DLC BTC balances + const dlcBTCBalance = await dlcBTC.balanceOf(poolMerchant.address); + console.log('\nDLC BTC balances:'); + console.log(' - PoolMerchant:', dlcBTCBalance.toString()); + + // Step 4: Separately check for rewards + console.log('\nšŸŒ¾ Checking reward state...'); + const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + MAINNET_ADDRESSES.CRV_TOKEN + ); + console.log('Current reward state:'); + console.log( + ' - Last claimed:', + new Date(lastClaimed * 1000).toISOString() + ); + console.log( + ' - Pending amount:', + ethers.utils.formatEther(pendingAmount) + ); + + // Step 5: Try harvesting rewards separately + console.log('\nTrying manual reward harvest...'); + try { + await poolMerchant + .connect(harvester) + .harvestRewardsForIntegration(curveIntegration.address, { + gasLimit: 2000000, + }); + console.log('Manual harvest successful'); + } catch (harvestError) { + console.log('Manual harvest failed:', harvestError.message); + } + } catch (error) { + console.log('\nāŒ Initial withdrawal failed:', error.message); + } + + console.log('\nāœ… Test sequence complete'); } main() diff --git a/test/PoolMerchant.test.js b/test/PoolMerchant.test.js index 9f357ce..86a75ec 100644 --- a/test/PoolMerchant.test.js +++ b/test/PoolMerchant.test.js @@ -4,6 +4,12 @@ const hardhat = require('hardhat'); const { getSignatures, setSigners, whitelistAddress } = require('./utils'); +async function getEventArg(tx, eventName, argName) { + const receipt = await tx.wait(); + const event = receipt.events.find((e) => e.event === eventName); + return event.args[argName]; +} + describe('PoolMerchant', () => { let poolMerchant; let dlcManager; @@ -112,25 +118,66 @@ describe('PoolMerchant', () => { }); }); - describe('Vault Creation', function () { - it('should create a pending vault', async function () { + describe('Vault Creation and Integration', function () { + beforeEach(async function () { + // Setup integration + await poolMerchant + .connect(deployer) + .addRewardToken(mockRewardToken.address); + await poolMerchant + .connect(deployer) + .setIntegration(mockIntegration.address, [ + mockRewardToken.address, + ]); + }); + + it('should create a pending vault with integration', async function () { const taprootPubKey = 'taproot123'; const withdrawalTxId = 'tx123'; const tx = await poolMerchant .connect(attestor_1) - .createPendingVault(taprootPubKey, withdrawalTxId); - await expect(tx).to.not.be.reverted; + .createPendingVault( + taprootPubKey, + withdrawalTxId, + mockIntegration.address + ); - const receipt = await tx.wait(); - const event = receipt.events.find( - (event) => event.event === 'PendingVaultCreated' + await expect(tx) + .to.emit(poolMerchant, 'PendingVaultCreated') + .withArgs( + await getEventArg(tx, 'PendingVaultCreated', 'uuid'), + taprootPubKey, + withdrawalTxId, + mockIntegration.address + ); + + const vaultId = await getEventArg( + tx, + 'PendingVaultCreated', + 'uuid' + ); + expect(await poolMerchant.getVaultIntegration(vaultId)).to.equal( + mockIntegration.address ); - const vaultId = event.args.uuid; + }); + + it('should not create vault with inactive integration', async function () { + const MockIntegration = + await ethers.getContractFactory('MockIntegration'); + const newIntegration = await MockIntegration.deploy([ + mockRewardToken.address, + ]); - // Verify vault was created in DLCManager - const UUID = (await dlcManager.getDLC(vaultId)).uuid; - expect(UUID).to.equal(vaultId); + await expect( + poolMerchant + .connect(attestor_1) + .createPendingVault( + 'taproot123', + 'tx123', + newIntegration.address + ) + ).to.be.revertedWith('Integration not active'); }); }); @@ -218,23 +265,11 @@ describe('PoolMerchant', () => { }); }); - describe('Vault Creation and Management', function () { + describe('Allocation and Rewards', function () { let vaultId; - const taprootPubKey = 'taproot123'; - const withdrawalTxId = 'tx123'; - const valueLocked = 1000000; + const initialFunding = ethers.utils.parseUnits('1', 8); beforeEach(async function () { - const tx = await poolMerchant - .connect(attestor_1) - .createPendingVault(taprootPubKey, withdrawalTxId); - const receipt = await tx.wait(); - vaultId = receipt.events.find( - (event) => event.event === 'PendingVaultCreated' - ).args.uuid; - }); - - it('should allocate to integration', async function () { // Setup integration await poolMerchant .connect(deployer) @@ -245,229 +280,108 @@ describe('PoolMerchant', () => { mockRewardToken.address, ]); - const signatureBytesForFunding = await getSignatures( - { - uuid: vaultId, - btcTxId: withdrawalTxId, - functionString: 'set-status-funded', - newLockedAmount: valueLocked, - }, - attestors, - 3 - ); - const tx3 = await dlcManager + // Create vault + const tx = await poolMerchant .connect(attestor_1) - .setStatusFunded( - vaultId, - withdrawalTxId, - signatureBytesForFunding, - valueLocked + .createPendingVault( + 'taproot123', + 'tx123', + mockIntegration.address ); - await tx3.wait(); - - // Allocate - await expect( - poolMerchant - .connect(operator) - .allocateToIntegration(vaultId, mockIntegration.address) - ) - .to.emit(poolMerchant, 'SharesAllocated') - .withArgs(vaultId, mockIntegration.address, valueLocked); // Assuming 1:1 share ratio - - const shares = await poolMerchant.getVaultShares( - vaultId, - mockIntegration.address - ); - expect(shares).to.equal(valueLocked); - }); - - it('should not allocate unfunded vault', async function () { - await poolMerchant - .connect(deployer) - .addRewardToken(mockRewardToken.address); - await poolMerchant - .connect(deployer) - .setIntegration(mockIntegration.address, [ - mockRewardToken.address, - ]); + vaultId = await getEventArg(tx, 'PendingVaultCreated', 'uuid'); - await expect( - poolMerchant - .connect(operator) - .allocateToIntegration(vaultId, mockIntegration.address) - ).to.be.revertedWith('Vault not funded'); - }); - }); - - describe('Reward Harvesting and Claims', function () { - let vaultId; - let mockRewardToken2; - const withdrawalTxId = 'tx123'; - const valueLocked = 1000000; - const initialFunding = ethers.utils.parseUnits('1', 8); - - beforeEach(async function () { - // Create and fund vault - const tx = await poolMerchant - .connect(attestor_1) - .createPendingVault('taproot123', 'tx123'); - const receipt = await tx.wait(); - vaultId = receipt.events.find( - (e) => e.event === 'PendingVaultCreated' - ).args.uuid; + // Fund vault const signatureBytesForFunding = await getSignatures( { uuid: vaultId, - btcTxId: withdrawalTxId, + btcTxId: 'tx123', functionString: 'set-status-funded', newLockedAmount: initialFunding, }, attestors, 3 ); - const tx3 = await dlcManager + await dlcManager .connect(attestor_1) .setStatusFunded( vaultId, - withdrawalTxId, + 'tx123', signatureBytesForFunding, initialFunding ); - await tx3.wait(); - - // Deploy second mock reward token (unsupported) - const MockERC20 = await ethers.getContractFactory('MockERC20'); - mockRewardToken2 = await MockERC20.deploy( - 'Mock Reward 2', - 'MRWD2', - 18 - ); + }); - // Deploy integration with both reward tokens - const MockIntegration = - await ethers.getContractFactory('MockIntegration'); - mockIntegration = await MockIntegration.deploy([ - mockRewardToken.address, - mockRewardToken2.address, - ]); + it('should allocate to integration', async function () { + await expect( + poolMerchant.connect(operator).allocateToIntegration(vaultId) + ) + .to.emit(poolMerchant, 'SharesAllocated') + .withArgs(vaultId, mockIntegration.address, initialFunding); // Assuming 1:1 share ratio - // Setup supported reward token and integration - await poolMerchant - .connect(deployer) - .addRewardToken(mockRewardToken.address); - await poolMerchant - .connect(deployer) - .setIntegration(mockIntegration.address, [ - mockRewardToken.address, - ]); - await poolMerchant - .connect(operator) - .allocateToIntegration(vaultId, mockIntegration.address); + const shares = await poolMerchant.getVaultShares(vaultId); + expect(shares).to.equal(initialFunding); - // Fund mock integration with rewards - await mockRewardToken.mint( - mockIntegration.address, - ethers.utils.parseEther('100') - ); - await mockRewardToken2.mint( - mockIntegration.address, - ethers.utils.parseEther('100') - ); + const details = + await poolMerchant.getVaultAllocationDetails(vaultId); + expect(details.allocated).to.equal(initialFunding); + expect(details.unallocated).to.equal(0); }); - it('should harvest and distribute only supported rewards', async function () { - // Mock pending rewards in integration - const supportedAmount = ethers.utils.parseEther('10'); - const unsupportedAmount = ethers.utils.parseEther('5'); - await mockIntegration.mockRewards([ - supportedAmount, - unsupportedAmount, - ]); + it('should harvest rewards during withdrawal', async function () { + // First allocate + await poolMerchant.connect(operator).allocateToIntegration(vaultId); + // Mock some rewards + const rewardAmount = ethers.utils.parseEther('10'); + await mockRewardToken.mint(mockIntegration.address, rewardAmount); + await mockIntegration.mockRewards([rewardAmount]); + + // Withdraw partial amount + const withdrawAmount = initialFunding.div(2); + + // Should emit both rewards harvested and withdrawal events await expect( poolMerchant - .connect(harvester) - .harvestRewards(mockIntegration.address) + .connect(attestor_1) + .withdrawFromVault(vaultId, withdrawAmount) ) .to.emit(poolMerchant, 'RewardsHarvested') .withArgs( mockIntegration.address, mockRewardToken.address, - harvester.address, - supportedAmount - ); + poolMerchant.address, + rewardAmount + ) + .and.to.emit(poolMerchant, 'VaultWithdrawn') + .withArgs(vaultId, withdrawAmount); - // Check only supported token was distributed + // Check rewards were distributed const [_, pendingAmount] = await poolMerchant.getVaultReward( vaultId, - mockIntegration.address, mockRewardToken.address ); - expect(pendingAmount).to.equal(supportedAmount); - - // Verify unsupported token was claimed but not distributed - expect( - await mockRewardToken2.balanceOf(poolMerchant.address) - ).to.equal(unsupportedAmount); + expect(pendingAmount).to.equal(rewardAmount); }); - it('should allow users to claim rewards', async function () { - await mockIntegration.mockRewards([ - ethers.utils.parseEther('10'), - ethers.utils.parseEther('5'), - ]); - await poolMerchant - .connect(harvester) - .harvestRewards(mockIntegration.address); + it('should still allow manual reward harvesting by harvester', async function () { + await poolMerchant.connect(operator).allocateToIntegration(vaultId); + + const rewardAmount = ethers.utils.parseEther('10'); + await mockRewardToken.mint(mockIntegration.address, rewardAmount); + await mockIntegration.mockRewards([rewardAmount]); await expect( poolMerchant - .connect(user) - .claimRewards( - vaultId, - mockIntegration.address, - mockRewardToken.address - ) + .connect(harvester) + .harvestRewardsForIntegration(mockIntegration.address) ) - .to.emit(poolMerchant, 'RewardsClaimed') + .to.emit(poolMerchant, 'RewardsHarvested') .withArgs( - vaultId, mockIntegration.address, mockRewardToken.address, - ethers.utils.parseEther('10') + poolMerchant.address, + rewardAmount ); - - // Verify reward was claimed - const [_, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - mockIntegration.address, - mockRewardToken.address - ); - expect(pendingAmount).to.equal(0); - expect(await mockRewardToken.balanceOf(user.address)).to.equal( - ethers.utils.parseEther('10') - ); - }); - - it('should handle rewards when no tokens are supported', async function () { - // Deploy integration with only unsupported reward - const newIntegration = await ( - await ethers.getContractFactory('MockIntegration') - ).deploy([mockRewardToken2.address]); - - await poolMerchant - .connect(deployer) - .setIntegration(newIntegration.address, []); - await mockRewardToken2.mint( - newIntegration.address, - ethers.utils.parseEther('10') - ); - await newIntegration.mockRewards([ethers.utils.parseEther('10')]); - - // Should not revert but no rewards distributed - await poolMerchant - .connect(harvester) - .harvestRewards(newIntegration.address); }); }); }); From f8dcc6d96d9c8ce041c24a4585622e314e57ec25 Mon Sep 17 00:00:00 2001 From: Rayerleier <1045643889@qq.com> Date: Tue, 5 Nov 2024 18:30:39 +0800 Subject: [PATCH 14/27] integrationSample.sol --- contracts/PoolMerchant.sol | 17 +- contracts/integrations/intergrationSample.sol | 113 ++++++ contracts/mocks/MockERC4626.sol | 77 ++++ ...ol_merchant_flow_with_integrationSample.js | 332 ++++++++++++++++++ 4 files changed, 531 insertions(+), 8 deletions(-) create mode 100644 contracts/integrations/intergrationSample.sol create mode 100644 contracts/mocks/MockERC4626.sol create mode 100644 scripts/pool_merchant_flow_with_integrationSample.js diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 136652e..7c4411f 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -27,6 +27,7 @@ interface IDLCManager { ) external returns (bytes32); function withdraw(bytes32 uuid, uint256 amount) external; + function getDLC(bytes32 uuid) external view returns (DLCLink.DLC memory); } @@ -197,14 +198,14 @@ contract PoolMerchant is } // Calculate shares to withdraw based on requested amount - uint256 sharesToWithdraw; - if (amount == vault.allocated) { - // If withdrawing all, withdraw all shares - sharesToWithdraw = vault.shares; - } else { - // Otherwise, withdraw proportional shares - sharesToWithdraw = (vault.shares * amount) / vault.allocated; - } + uint256 sharesToWithdraw = amount; + // if (amount == vault.allocated) { + // // If withdrawing all, withdraw all shares + // sharesToWithdraw = vault.shares; + // } else { + // // Otherwise, withdraw proportional shares + // sharesToWithdraw = (vault.shares * amount) / vault.allocated; + // } // Withdraw from integration uint256 received = integ.strategy.withdraw(sharesToWithdraw); diff --git a/contracts/integrations/intergrationSample.sol b/contracts/integrations/intergrationSample.sol new file mode 100644 index 0000000..433a15e --- /dev/null +++ b/contracts/integrations/intergrationSample.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "../interfaces/IIntegration.sol"; +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract IntegrationSample is IIntegration, Ownable { + uint256 public rewardRatePerSecond; + uint256 public lastRewardTime; + uint256 public pendingRewards; + + address[] public rewardTokens; + IERC4626 public vault; + IERC20 public rewardToken; + + constructor( + IERC4626 _vault, + IERC20 _rewardToken, + uint256 _rewardRatePerSecond, + address _poolMerchant + ) { + vault = _vault; + rewardToken = _rewardToken; + rewardRatePerSecond = _rewardRatePerSecond; + lastRewardTime = block.timestamp; + rewardTokens.push(address(_rewardToken)); + transferOwnership(_poolMerchant); + } + + // NOTE: need to use transferFrom + function deposit( + uint256 amount + ) external override onlyOwner returns (uint256 shares) { + require(amount > 0, "Amount must be greater than 0"); + _updateRewards(); + IERC20 asset = IERC20(vault.asset()); + asset.approve(address(vault), amount); + shares = vault.deposit(amount, address(this)); + return shares; + } + + function withdraw( + uint256 shares + ) external override onlyOwner returns (uint256 assets) { + require(shares > 0, "Shares must be greater than 0"); + + // Update rewards + _updateRewards(); + + // Check if we have enough shares + uint256 availableShares = vault.balanceOf(address(this)); + require(availableShares >= shares, "Not enough shares"); + + // First redeem shares from vault to get assets + assets = vault.redeem(shares, msg.sender, address(this)); + + return assets; + } + + function claimRewards() + external + override + onlyOwner + returns (uint256[] memory amounts) + { + _updateRewards(); + uint256 pendingReward = pendingRewards; + require(pendingReward > 0, "No rewards to claim"); + pendingRewards = 0; + + amounts = new uint256[](rewardTokens.length); + amounts[0] = pendingReward; + rewardToken.transfer(msg.sender, pendingReward); + } + + function getRewardTokens() + external + view + override + returns (address[] memory) + { + return rewardTokens; + } + + function getPendingRewards() + external + view + override + returns (uint256[] memory amounts) + { + uint256 rewards = _calculateRewards(); + amounts = new uint256[](rewardTokens.length); + amounts[0] = pendingRewards + rewards; + } + + function _updateRewards() internal { + uint256 rewards = _calculateRewards(); + pendingRewards += rewards; + lastRewardTime = block.timestamp; + } + + function _calculateRewards() internal view returns (uint256) { + uint256 timeElapsed = block.timestamp - lastRewardTime; + uint256 userShares = vault.balanceOf(address(this)); + uint256 totalShares = vault.totalSupply(); + if (totalShares == 0) { + return 0; + } + return (timeElapsed * rewardRatePerSecond * userShares) / totalShares; + } +} diff --git a/contracts/mocks/MockERC4626.sol b/contracts/mocks/MockERC4626.sol new file mode 100644 index 0000000..df6beef --- /dev/null +++ b/contracts/mocks/MockERC4626.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockERC4626Vault is ERC4626 { + constructor( + IERC20Metadata _asset + ) ERC20("Mock Vault Token", "MVT") ERC4626(_asset) {} + + // function deposit( + // uint256 assets, + // address receiver + // ) public override returns (uint256) { + // require(assets > 0, "Assets must be greater than 0"); + // IERC20(asset()).transferFrom(msg.sender, address(this), assets); + // uint256 shares = convertToShares(assets); + // require(shares > 0, "Shares must be greater than 0"); + // _mint(receiver, shares); + // emit Deposit(msg.sender, receiver, assets, shares); + // return shares; + // } + + // function totalAssets() public view override returns (uint256) { + // return IERC20(asset()).balanceOf(address(this)); + // } + + // function convertToShares( + // uint256 assets + // ) public view override returns (uint256) { + // uint256 supply = totalSupply(); + // uint256 totalAsset = totalAssets(); + // if (supply == 0 || totalAsset == 0) { + // return assets; + // } else { + // return (assets * supply) / totalAsset; + // } + // } + + // function convertToAssets( + // uint256 shares + // ) public view override returns (uint256) { + // uint256 supply = totalSupply(); + // uint256 totalAsset = totalAssets(); + // if (supply == 0 || totalAsset == 0) { + // return shares; + // } else { + // return (shares * totalAsset) / supply; + // } + // } + + // function redeem( + // uint256 shares, + // address receiver, + // address owner + // ) public override returns (uint256) { + // require(shares > 0, "Shares must be greater than 0"); + // require(balanceOf(owner) >= shares, "Insufficient shares"); + + // if (msg.sender != owner) { + // uint256 allowed = allowance(owner, msg.sender); + // require(allowed >= shares, "ERC20: insufficient allowance"); + // _approve(owner, msg.sender, allowed - shares); + // } + + // uint256 assets = convertToAssets(shares); + // uint256 totalAsset = totalAssets(); + // require(totalAsset >= assets, "Vault does not have enough assets"); + + // _burn(owner, shares); + // require(IERC20(asset()).transfer(receiver, assets), "Transfer failed"); + + // emit Withdraw(msg.sender, receiver, owner, assets, shares); + // return assets; + // } +} diff --git a/scripts/pool_merchant_flow_with_integrationSample.js b/scripts/pool_merchant_flow_with_integrationSample.js new file mode 100644 index 0000000..a7871ea --- /dev/null +++ b/scripts/pool_merchant_flow_with_integrationSample.js @@ -0,0 +1,332 @@ +const hre = require('hardhat'); +const { ethers, upgrades } = require('hardhat'); +const { getSignatures, whitelistAddress } = require('../test/utils'); + +async function fundAccount(address) { + await hre.network.provider.send('hardhat_setBalance', [ + address, + '0x2000000000000000000', + ]); +} + +async function main() { + console.log('\nšŸ”Ø Compiling contracts...'); + await hre.run('compile'); + + console.log('\nšŸš€ Starting happy path integration test...'); + + const MAINNET_ADDRESSES = { + DLC_MANAGER: '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', + DLC_BTC: '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', + }; + + const accounts = await ethers.getSigners(); + const deployer = accounts[0]; + const dlcAdmin = await ethers.getImpersonatedSigner( + '0xaA2949C5285C2f2887ABD567865344240c29d619' + ); + const dlcCritical = await ethers.getImpersonatedSigner( + '0x24f75096ad315Ab617a3d0f2621aC3e9D391Aa77' + ); + const attestor_1 = await ethers.getImpersonatedSigner( + '0x989E9c4005ABc2a8E4b85544B44d2d95cfDe08de' + ); + const attestor_2 = await ethers.getImpersonatedSigner( + '0xBe4aAE47A62f67bdF93eA9f5F189ae51B1b54492' + ); + const attestor_3 = await ethers.getImpersonatedSigner( + '0x7B254D8C6eBd9662A52180B06920aEA4f23a8940' + ); + const attestor_4 = await ethers.getImpersonatedSigner( + '0x194c697e8343EaB3C53917BA7e597d02687f8BA0' + ); + const attestor_5 = await ethers.getImpersonatedSigner( + '0x2daef70747eb9E97E5f31A9EBDbda593918F8bE7' + ); + const operator = deployer; + const harvester = deployer; + + const attestors = [ + attestor_1, + attestor_2, + attestor_3, + attestor_4, + attestor_5, + ]; + + console.log('\nšŸ’° Funding impersonated accounts...'); + for (const account of [dlcAdmin, ...attestors]) { + await fundAccount(account.address); + } + + console.log('\nšŸ”„ Upgrading DLCManager...'); + const proxyAdminAddress = await upgrades.erc1967.getAdminAddress( + MAINNET_ADDRESSES.DLC_MANAGER + ); + console.log('Proxy admin address:', proxyAdminAddress); + const connectedProxyAdmin = new ethers.Contract( + proxyAdminAddress, + [ + 'function owner() view returns (address)', + 'function upgrade(address, address) external', + ], + dlcCritical + ); + + const DLCManager = await ethers.getContractFactory('DLCManager'); + const dlcManagerImpl = await DLCManager.deploy(); + await dlcManagerImpl.deployed(); + + await connectedProxyAdmin.upgrade( + MAINNET_ADDRESSES.DLC_MANAGER, + dlcManagerImpl.address + ); + + console.log('DLCManager upgraded'); + + console.log('\nšŸ“ Getting contract instances...'); + const dlcManager = await ethers.getContractAt( + 'DLCManager', + MAINNET_ADDRESSES.DLC_MANAGER + ); + const dlcBTC = await ethers.getContractAt( + 'DLCBTC', + MAINNET_ADDRESSES.DLC_BTC + ); + + await dlcManager.connect(dlcAdmin).setSkipSignatureVerification(true); + + console.log('\nšŸ—ļø Deploying PoolMerchant...'); + const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + MAINNET_ADDRESSES.DLC_MANAGER, + MAINNET_ADDRESSES.DLC_BTC, + deployer.address, + ]); + await poolMerchant.deployed(); + console.log('PoolMerchant deployed to:', poolMerchant.address); + + await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); + + console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); + const MockERC4626Vault = + await ethers.getContractFactory('MockERC4626Vault'); + const mockVault = await MockERC4626Vault.deploy(MAINNET_ADDRESSES.DLC_BTC); + await mockVault.deployed(); + console.log('MockERC4626Vault deployed to:', mockVault.address); + + console.log('\nšŸ—ļø Deploying IntegrationSample...'); + const IntegrationSample = + await ethers.getContractFactory('IntegrationSample'); + const rewardRatePerSecond = ethers.utils.parseUnits('1', 8); + + const integrationSample = await IntegrationSample.deploy( + mockVault.address, + MAINNET_ADDRESSES.DLC_BTC, + rewardRatePerSecond, + poolMerchant.address + ); + await integrationSample.deployed(); + console.log('IntegrationSample deployed to:', integrationSample.address); + + console.log('\nšŸ”‘ Setting up roles and integration...'); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + attestor_1.address + ); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + operator.address + ); + await poolMerchant.grantRole( + await poolMerchant.OPERATOR_ROLE(), + operator.address + ); + await poolMerchant.grantRole( + await poolMerchant.HARVESTER_ROLE(), + harvester.address + ); + + console.log('\nšŸŖ™ Adding DLCBTC as reward token...'); + await poolMerchant.addRewardToken(MAINNET_ADDRESSES.DLC_BTC); + + console.log('\nšŸ”„ Setting up IntegrationSample...'); + await poolMerchant.setIntegration(integrationSample.address, [ + MAINNET_ADDRESSES.DLC_BTC, + ]); + + console.log('\nšŸ“¦ Creating vault with IntegrationSample...'); + const mockBtcTxId = '0x123'; + const mockTaprootPubkey = '0x12345'; + + const tx = await poolMerchant + .connect(attestor_1) + .createPendingVault( + mockTaprootPubkey, + mockBtcTxId, + integrationSample.address, + { + gasLimit: 1000000, + } + ); + + const receipt = await tx.wait(); + const vaultId = receipt.events.find( + (e) => e.event === 'PendingVaultCreated' + ).args.uuid; + console.log('Vault created with ID:', vaultId); + + console.log('\nšŸ’° Funding vault...'); + const fundAmount = ethers.utils.parseUnits('1', 8); + console.log('Funding amount:', fundAmount.toString()); + + const tx3 = await dlcManager + .connect(attestor_1) + .setStatusFunded(vaultId, mockBtcTxId, [], fundAmount); + await tx3.wait(); + + console.log( + '\nšŸ”„ PoolMerchant transferring dlcBTC to IntegrationSample...' + ); + await poolMerchant + .connect(operator) + .transferDLCBTC(integrationSample.address, fundAmount); + console.log('Transfer successful'); + + console.log('\nšŸ“ˆ Allocating to IntegrationSample...'); + + const integrationSharesBefore = await mockVault.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample shares before allocation:', + integrationSharesBefore.toString() + ); + + await poolMerchant.connect(operator).allocateToIntegration(vaultId); + + const shares = await poolMerchant.getVaultShares(vaultId); + console.log('Allocated shares:', shares.toString()); + + const integrationSharesAfter = await mockVault.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample shares after allocation:', + integrationSharesAfter.toString() + ); + + console.log('\nā³ Mining blocks to accrue rewards...'); + await hre.network.provider.send('hardhat_mine', ['0x100']); + + console.log( + '\nšŸ¦ Performing partial withdrawal to trigger reward harvest...' + ); + const withdrawAmount = fundAmount.div(2); + const sharesToWithdraw = withdrawAmount; + + console.log('\nšŸ” Testing withdrawal process...'); + + const vaultBefore = await poolMerchant.getVaultAllocationDetails(vaultId); + const sharesBefore = await poolMerchant.getVaultShares(vaultId); + console.log('\nInitial state:'); + console.log(' - Total minted:', vaultBefore.valueMinted.toString()); + console.log(' - Allocated:', vaultBefore.allocated.toString()); + console.log(' - Shares:', sharesBefore.toString()); + + const assetToken = await ethers.getContractAt( + 'IERC20', + MAINNET_ADDRESSES.DLC_BTC + ); + + const integrationBalance = await assetToken.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample asset balance:', + integrationBalance.toString() + ); + + const vaultBalance = await assetToken.balanceOf(mockVault.address); + console.log('MockERC4626Vault asset balance:', vaultBalance.toString()); + + console.log( + '\nAttempting withdrawal of shares:', + sharesToWithdraw.toString() + ); + try { + const integrationAssetBalance = await mockVault.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample asset balance before withdrawal:', + integrationAssetBalance.toString() + ); + + if (integrationAssetBalance.lt(withdrawAmount)) { + console.log( + 'āš ļø IntegrationSample does not have enough balance to withdraw' + ); + } + + const withdrawTx = await poolMerchant + .connect(operator) + .withdrawFromVault(vaultId, sharesToWithdraw, { + gasLimit: 2000000, + }); + + await withdrawTx.wait(); + console.log('Withdrawal successful!'); + + const vaultAfter = + await poolMerchant.getVaultAllocationDetails(vaultId); + const sharesAfter = await poolMerchant.getVaultShares(vaultId); + console.log('\nPost-withdrawal state:'); + console.log(' - Total minted:', vaultAfter.valueMinted.toString()); + console.log(' - Allocated:', vaultAfter.allocated.toString()); + console.log(' - Shares:', sharesAfter.toString()); + + const dlcBTCBalance = await dlcBTC.balanceOf(poolMerchant.address); + console.log('\nDLCBTC balances:'); + console.log(' - PoolMerchant:', dlcBTCBalance.toString()); + + const integrationAssetBalanceAfter = await dlcBTC.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample asset balance after withdrawal:', + integrationAssetBalanceAfter.toString() + ); + + const vaultAssetBalanceAfter = await dlcBTC.balanceOf( + mockVault.address + ); + console.log( + 'MockERC4626Vault asset balance after withdrawal:', + vaultAssetBalanceAfter.toString() + ); + } catch (error) { + console.log('\nāŒ Withdrawal failed:', error.message); + + const integrationBalanceAfter = await assetToken.balanceOf( + integrationSample.address + ); + console.log( + 'IntegrationSample asset balance after withdrawal attempt:', + integrationBalanceAfter.toString() + ); + + const vaultBalanceAfter = await assetToken.balanceOf(mockVault.address); + console.log( + 'MockERC4626Vault asset balance after withdrawal attempt:', + vaultBalanceAfter.toString() + ); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From a0d8083de8fc65587e1889b35df4c93f1f28dc3a Mon Sep 17 00:00:00 2001 From: scolear Date: Tue, 5 Nov 2024 11:35:56 +0100 Subject: [PATCH 15/27] feat: add uuid-taproot mapping --- contracts/PoolMerchant.sol | 57 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 7c4411f..b53d417 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -81,11 +81,11 @@ contract PoolMerchant is } mapping(bytes32 => VaultInfo) internal _vaults; + mapping(string => bytes32[]) public vaultsByBitcoinAddress; mapping(address => Integration) public integrations; mapping(address => RewardToken) public rewardTokens; address[] public activeIntegrations; - uint256 public totalValueLocked; uint256[50] private __gap; //////////////////////////////////////////////////////////////// @@ -169,6 +169,8 @@ contract PoolMerchant is _vaults[_uuid].integration = integration; _addVaultToIntegration(_uuid, integration); + vaultsByBitcoinAddress[taprootPubKey].push(_uuid); + emit PendingVaultCreated( _uuid, taprootPubKey, @@ -269,7 +271,6 @@ contract PoolMerchant is uint256 vaultReward = (amount * vault.shares) / integ.totalShares; reward.pendingAmount += vaultReward; - reward.lastClaimedAt = block.timestamp; emit RewardsHarvested( integration, @@ -307,6 +308,7 @@ contract PoolMerchant is uint256 amount = reward.pendingAmount; reward.pendingAmount = 0; + reward.lastClaimedAt = block.timestamp; require( IERC20(rewardToken).transfer(msg.sender, amount), @@ -490,6 +492,57 @@ contract PoolMerchant is amounts = integ.strategy.getPendingRewards(); } + //////////////////////////////////////////////////////////////// + // VAULT QUERIES // + //////////////////////////////////////////////////////////////// + + // Get all vault UUIDs for a taproot public key + function getVaultsByBitcoinAddress( + string calldata taprootPubKey + ) external view returns (bytes32[] memory) { + return vaultsByBitcoinAddress[taprootPubKey]; + } + + // Get details for a specific vault by UUID + function getVaultDetails( + bytes32 uuid, + address[] calldata _rewardTokens + ) + external + view + returns ( + address integration, + uint256 shares, + uint256 valueMinted, + uint256 allocated, + uint256 unallocated, + uint256[] memory lastClaimedAt, + uint256[] memory pendingAmounts + ) + { + require(uuid != bytes32(0), "Invalid UUID"); + VaultInfo storage vault = _vaults[uuid]; + require(vault.integration != address(0), "Vault not found"); + + DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); + + integration = vault.integration; + shares = vault.shares; + valueMinted = dlc.valueMinted; + allocated = vault.allocated; + unallocated = valueMinted - allocated; + + // Get reward data + lastClaimedAt = new uint256[](_rewardTokens.length); + pendingAmounts = new uint256[](_rewardTokens.length); + + for (uint256 i = 0; i < _rewardTokens.length; i++) { + UserReward storage reward = vault.rewards[_rewardTokens[i]]; + lastClaimedAt[i] = reward.lastClaimedAt; + pendingAmounts[i] = reward.pendingAmount; + } + } + //////////////////////////////////////////////////////////////// // ADMIN FUNCTIONS // //////////////////////////////////////////////////////////////// From 0978eadcc505eddc35c839dc5705bd908ca3a2b5 Mon Sep 17 00:00:00 2001 From: scolear Date: Tue, 5 Nov 2024 17:08:45 +0100 Subject: [PATCH 16/27] feat: working IntegrationSample --- contracts/PoolMerchant.sol | 81 ++++--- contracts/integrations/intergrationSample.sol | 195 ++++++++++++++--- contracts/mocks/MockERC4626.sol | 66 ------ ...ol_merchant_flow_with_integrationSample.js | 204 ++++++++++++++---- 4 files changed, 369 insertions(+), 177 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index b53d417..4ccf78b 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -19,6 +19,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "./DLCLinkLibrary.sol"; import "./interfaces/IIntegration.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; interface IDLCManager { function setupPendingVault( @@ -42,6 +43,7 @@ contract PoolMerchant is { using DLCLink for DLCLink.DLC; using DLCLink for DLCLink.DLCStatus; + using SafeMath for uint256; //////////////////////////////////////////////////////////////// // STATE VARIABLES // @@ -199,48 +201,26 @@ contract PoolMerchant is _harvestRewardsForVault(uuid); } - // Calculate shares to withdraw based on requested amount - uint256 sharesToWithdraw = amount; - // if (amount == vault.allocated) { - // // If withdrawing all, withdraw all shares - // sharesToWithdraw = vault.shares; - // } else { - // // Otherwise, withdraw proportional shares - // sharesToWithdraw = (vault.shares * amount) / vault.allocated; - // } - // Withdraw from integration - uint256 received = integ.strategy.withdraw(sharesToWithdraw); - - // Update state based on what we actually received - vault.shares -= sharesToWithdraw; - integrations[integration].totalShares -= sharesToWithdraw; - - // If we received less than requested, we need to adjust the withdrawal amount - uint256 withdrawAmount; - if (received < amount) { - // We can only withdraw what we actually received - withdrawAmount = received; - // Adjust allocation down based on what we actually received - vault.allocated -= received; - } else { - // We got enough or more than requested - withdrawAmount = amount; - vault.allocated -= amount; - if (received > amount) { - // If we got extra, add it to allocation - vault.allocated += (received - amount); - } - } + uint256 received = integ.strategy.withdraw(amount); + + vault.shares = vault.shares.sub(received, "Insufficient shares"); + vault.allocated = vault.allocated.sub( + received, + "Insufficient allocation" + ); + integrations[integration].totalShares = integrations[integration] + .totalShares + .sub(received, "Insufficient total shares"); // Clean up if fully withdrawn if (vault.shares == 0) { _removeVaultFromIntegration(uuid, integration); } - // Withdraw from DLCManager with adjusted amount - dlcManager.withdraw(uuid, withdrawAmount); - emit VaultWithdrawn(uuid, withdrawAmount); + // Withdraw from DLCManager with adjusted received + dlcManager.withdraw(uuid, received); + emit VaultWithdrawn(uuid, received); } function _harvestRewardsForVault(bytes32 uuid) internal { @@ -268,9 +248,10 @@ contract PoolMerchant is // Calculate this vault's share of the rewards if (amount > 0 && vault.shares > 0) { UserReward storage reward = vault.rewards[rewardAddress]; - uint256 vaultReward = (amount * vault.shares) / - integ.totalShares; - reward.pendingAmount += vaultReward; + uint256 vaultReward = amount.mul(vault.shares).div( + integ.totalShares + ); + reward.pendingAmount = reward.pendingAmount.add(vaultReward); emit RewardsHarvested( integration, @@ -362,7 +343,10 @@ contract PoolMerchant is DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.valueMinted > 0, "Vault not funded"); - uint256 unallocated = dlc.valueMinted - vault.allocated; + uint256 unallocated = dlc.valueMinted.sub( + vault.allocated, + "Already fully allocated" + ); require(unallocated > 0, "Nothing to allocate"); require(dlcBTC.approve(integration, unallocated), "Approval failed"); @@ -371,9 +355,11 @@ contract PoolMerchant is unallocated ); - vault.shares += shares; - vault.allocated += unallocated; - integrations[integration].totalShares += shares; + vault.shares = vault.shares.add(shares); + vault.allocated = vault.allocated.add(unallocated); + integrations[integration].totalShares = integrations[integration] + .totalShares + .add(shares); emit SharesAllocated(uuid, integration, shares); } @@ -470,12 +456,19 @@ contract PoolMerchant is valueMinted = dlc.valueMinted; allocated = vault.allocated; - unallocated = valueMinted - allocated; + unallocated = valueMinted.sub( + allocated, + "Allocation exceeds minted value" + ); } function getUnallocatedAmount(bytes32 uuid) public view returns (uint256) { DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); - return dlc.valueMinted - _vaults[uuid].allocated; + return + dlc.valueMinted.sub( + _vaults[uuid].allocated, + "Allocation exceeds minted value" + ); } function getPendingIntegrationRewards( diff --git a/contracts/integrations/intergrationSample.sol b/contracts/integrations/intergrationSample.sol index 433a15e..da3cc00 100644 --- a/contracts/integrations/intergrationSample.sol +++ b/contracts/integrations/intergrationSample.sol @@ -5,38 +5,66 @@ import "../interfaces/IIntegration.sol"; import "@openzeppelin/contracts/interfaces/IERC4626.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract IntegrationSample is IIntegration, Ownable { + using SafeERC20 for IERC20; + using SafeERC20 for IERC4626; + uint256 public rewardRatePerSecond; uint256 public lastRewardTime; - uint256 public pendingRewards; + uint256 public accumulatedRewardPerShare; + uint256 public rewardBalance; // Track reward token balance to distribute + + mapping(address => uint256) public userRewardDebt; address[] public rewardTokens; IERC4626 public vault; IERC20 public rewardToken; + address public poolMerchant; + address public dlcBTC; + event RewardsUpdated( + uint256 timeElapsed, + uint256 rewardsAdded, + uint256 newAccumulatedRewardPerShare + ); + + event RewardsClaimed(address user, uint256 amount); constructor( IERC4626 _vault, IERC20 _rewardToken, uint256 _rewardRatePerSecond, - address _poolMerchant + address _poolMerchant, + address _dlcBTC ) { vault = _vault; rewardToken = _rewardToken; rewardRatePerSecond = _rewardRatePerSecond; lastRewardTime = block.timestamp; rewardTokens.push(address(_rewardToken)); + poolMerchant = _poolMerchant; transferOwnership(_poolMerchant); + dlcBTC = _dlcBTC; } - // NOTE: need to use transferFrom function deposit( uint256 amount ) external override onlyOwner returns (uint256 shares) { require(amount > 0, "Amount must be greater than 0"); _updateRewards(); - IERC20 asset = IERC20(vault.asset()); - asset.approve(address(vault), amount); + IERC20 _dlcBTC = IERC20(dlcBTC); + + require( + _dlcBTC.allowance(msg.sender, address(this)) >= amount, + "Not enough allowance" + ); + require(_dlcBTC == IERC20(vault.asset()), "Invalid asset"); + + _dlcBTC.safeTransferFrom(msg.sender, address(this), amount); + _dlcBTC.safeApprove(address(vault), 0); // Clear any existing approval + _dlcBTC.safeApprove(address(vault), amount); + shares = vault.deposit(amount, address(this)); return shares; } @@ -44,21 +72,36 @@ contract IntegrationSample is IIntegration, Ownable { function withdraw( uint256 shares ) external override onlyOwner returns (uint256 assets) { + _updateRewards(); require(shares > 0, "Shares must be greater than 0"); + require( + IERC20(address(vault)).balanceOf(address(this)) >= shares, + "Not enough shares" + ); - // Update rewards - _updateRewards(); + // Just do the approval step + IERC20(address(vault)).safeApprove(address(vault), 0); + IERC20(address(vault)).safeApprove(address(vault), shares); - // Check if we have enough shares - uint256 availableShares = vault.balanceOf(address(this)); - require(availableShares >= shares, "Not enough shares"); + assets = vault.redeem( + shares, + address(this), // receive assets here + address(this) // owner of shares + ); - // First redeem shares from vault to get assets - assets = vault.redeem(shares, msg.sender, address(this)); + IERC20(vault.asset()).safeTransfer(poolMerchant, assets); return assets; } + function updateRewardRate(uint256 newRate) external onlyOwner { + _updateRewards(); // Update with old rate first + rewardRatePerSecond = newRate; + } + + // NOTE: normally, such logic would be in the vault side of the flow, + // and we would just call it from here. + // But for this demo, we are keeping it here. function claimRewards() external override @@ -66,22 +109,31 @@ contract IntegrationSample is IIntegration, Ownable { returns (uint256[] memory amounts) { _updateRewards(); - uint256 pendingReward = pendingRewards; - require(pendingReward > 0, "No rewards to claim"); - pendingRewards = 0; - amounts = new uint256[](rewardTokens.length); - amounts[0] = pendingReward; - rewardToken.transfer(msg.sender, pendingReward); - } + uint256 totalShares = vault.balanceOf(address(this)); + require(totalShares > 0, "No shares"); - function getRewardTokens() - external - view - override - returns (address[] memory) - { - return rewardTokens; + uint256 pending = (totalShares * accumulatedRewardPerShare) / + 1e18 - + userRewardDebt[msg.sender]; + + if (pending > 0) { + require( + rewardToken.balanceOf(address(this)) >= pending, + "Insufficient reward balance" + ); + + userRewardDebt[msg.sender] = + (totalShares * accumulatedRewardPerShare) / + 1e18; + rewardToken.safeTransfer(msg.sender, pending); + + emit RewardsClaimed(msg.sender, pending); + } + + amounts = new uint256[](1); + amounts[0] = pending; + return amounts; } function getPendingRewards() @@ -90,17 +142,98 @@ contract IntegrationSample is IIntegration, Ownable { override returns (uint256[] memory amounts) { - uint256 rewards = _calculateRewards(); - amounts = new uint256[](rewardTokens.length); - amounts[0] = pendingRewards + rewards; + amounts = new uint256[](1); + + uint256 totalShares = vault.balanceOf(address(this)); + if (totalShares == 0) { + amounts[0] = 0; + return amounts; + } + + // Calculate rewards that would be added since last update + uint256 timeElapsed = block.timestamp - lastRewardTime; + uint256 currentBalance = rewardToken.balanceOf(address(this)); + uint256 maxRewardsToAdd = currentBalance > rewardBalance + ? timeElapsed * rewardRatePerSecond + : 0; + + uint256 currentAccRewardPerShare = accumulatedRewardPerShare; + if (maxRewardsToAdd > 0) { + currentAccRewardPerShare += (maxRewardsToAdd * 1e18) / totalShares; + } + + amounts[0] = + ((totalShares * currentAccRewardPerShare) / 1e18) - + userRewardDebt[msg.sender]; + return amounts; } function _updateRewards() internal { - uint256 rewards = _calculateRewards(); - pendingRewards += rewards; + uint256 totalShares = vault.balanceOf(address(this)); + if (totalShares == 0) { + lastRewardTime = block.timestamp; + return; + } + + uint256 timeElapsed = block.timestamp - lastRewardTime; + if (timeElapsed == 0) return; + + uint256 currentBalance = rewardToken.balanceOf(address(this)); + + // Only distribute rewards if we have new tokens to distribute + if (currentBalance > rewardBalance) { + uint256 maxRewardsToAdd = timeElapsed * rewardRatePerSecond; + uint256 actualRewardsToAdd = currentBalance - rewardBalance; + + // Use the smaller of maxRewardsToAdd or actualRewardsToAdd + uint256 rewardsToAdd = maxRewardsToAdd < actualRewardsToAdd + ? maxRewardsToAdd + : actualRewardsToAdd; + + accumulatedRewardPerShare += (rewardsToAdd * 1e18) / totalShares; + rewardBalance = currentBalance; + + emit RewardsUpdated( + timeElapsed, + rewardsToAdd, + accumulatedRewardPerShare + ); + } + lastRewardTime = block.timestamp; } + function getRewardState() + external + view + returns ( + uint256 totalShares, + uint256 currentRewardBalance, + uint256 trackedRewardBalance, + uint256 accRewardPerShare, + uint256 lastUpdate, + uint256 rewardRate + ) + { + return ( + vault.balanceOf(address(this)), + rewardToken.balanceOf(address(this)), + rewardBalance, + accumulatedRewardPerShare, + lastRewardTime, + rewardRatePerSecond + ); + } + + function getRewardTokens() + external + view + override + returns (address[] memory) + { + return rewardTokens; + } + function _calculateRewards() internal view returns (uint256) { uint256 timeElapsed = block.timestamp - lastRewardTime; uint256 userShares = vault.balanceOf(address(this)); diff --git a/contracts/mocks/MockERC4626.sol b/contracts/mocks/MockERC4626.sol index df6beef..f3603b7 100644 --- a/contracts/mocks/MockERC4626.sol +++ b/contracts/mocks/MockERC4626.sol @@ -8,70 +8,4 @@ contract MockERC4626Vault is ERC4626 { constructor( IERC20Metadata _asset ) ERC20("Mock Vault Token", "MVT") ERC4626(_asset) {} - - // function deposit( - // uint256 assets, - // address receiver - // ) public override returns (uint256) { - // require(assets > 0, "Assets must be greater than 0"); - // IERC20(asset()).transferFrom(msg.sender, address(this), assets); - // uint256 shares = convertToShares(assets); - // require(shares > 0, "Shares must be greater than 0"); - // _mint(receiver, shares); - // emit Deposit(msg.sender, receiver, assets, shares); - // return shares; - // } - - // function totalAssets() public view override returns (uint256) { - // return IERC20(asset()).balanceOf(address(this)); - // } - - // function convertToShares( - // uint256 assets - // ) public view override returns (uint256) { - // uint256 supply = totalSupply(); - // uint256 totalAsset = totalAssets(); - // if (supply == 0 || totalAsset == 0) { - // return assets; - // } else { - // return (assets * supply) / totalAsset; - // } - // } - - // function convertToAssets( - // uint256 shares - // ) public view override returns (uint256) { - // uint256 supply = totalSupply(); - // uint256 totalAsset = totalAssets(); - // if (supply == 0 || totalAsset == 0) { - // return shares; - // } else { - // return (shares * totalAsset) / supply; - // } - // } - - // function redeem( - // uint256 shares, - // address receiver, - // address owner - // ) public override returns (uint256) { - // require(shares > 0, "Shares must be greater than 0"); - // require(balanceOf(owner) >= shares, "Insufficient shares"); - - // if (msg.sender != owner) { - // uint256 allowed = allowance(owner, msg.sender); - // require(allowed >= shares, "ERC20: insufficient allowance"); - // _approve(owner, msg.sender, allowed - shares); - // } - - // uint256 assets = convertToAssets(shares); - // uint256 totalAsset = totalAssets(); - // require(totalAsset >= assets, "Vault does not have enough assets"); - - // _burn(owner, shares); - // require(IERC20(asset()).transfer(receiver, assets), "Transfer failed"); - - // emit Withdraw(msg.sender, receiver, owner, assets, shares); - // return assets; - // } } diff --git a/scripts/pool_merchant_flow_with_integrationSample.js b/scripts/pool_merchant_flow_with_integrationSample.js index a7871ea..70527df 100644 --- a/scripts/pool_merchant_flow_with_integrationSample.js +++ b/scripts/pool_merchant_flow_with_integrationSample.js @@ -95,8 +95,11 @@ async function main() { ); await dlcManager.connect(dlcAdmin).setSkipSignatureVerification(true); + await dlcManager + .connect(dlcAdmin) + .setMinterOnTokenContract(deployer.address); - console.log('\nšŸ—ļø Deploying PoolMerchant...'); + console.log('\nšŸ—ļø Deploying PoolMerchant...'); const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ MAINNET_ADDRESSES.DLC_MANAGER, @@ -108,23 +111,25 @@ async function main() { await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); - console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); + console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); const MockERC4626Vault = await ethers.getContractFactory('MockERC4626Vault'); const mockVault = await MockERC4626Vault.deploy(MAINNET_ADDRESSES.DLC_BTC); await mockVault.deployed(); console.log('MockERC4626Vault deployed to:', mockVault.address); - console.log('\nšŸ—ļø Deploying IntegrationSample...'); + console.log('\nšŸ—ļø Deploying IntegrationSample...'); const IntegrationSample = await ethers.getContractFactory('IntegrationSample'); - const rewardRatePerSecond = ethers.utils.parseUnits('1', 8); + + const rewardRatePerSecond = ethers.BigNumber.from('317'); const integrationSample = await IntegrationSample.deploy( mockVault.address, MAINNET_ADDRESSES.DLC_BTC, rewardRatePerSecond, - poolMerchant.address + poolMerchant.address, + dlcBTC.address ); await integrationSample.deployed(); console.log('IntegrationSample deployed to:', integrationSample.address); @@ -147,7 +152,7 @@ async function main() { harvester.address ); - console.log('\nšŸŖ™ Adding DLCBTC as reward token...'); + console.log('\nšŸŖ™ Adding DLCBTC as reward token...'); await poolMerchant.addRewardToken(MAINNET_ADDRESSES.DLC_BTC); console.log('\nšŸ”„ Setting up IntegrationSample...'); @@ -185,34 +190,26 @@ async function main() { .setStatusFunded(vaultId, mockBtcTxId, [], fundAmount); await tx3.wait(); - console.log( - '\nšŸ”„ PoolMerchant transferring dlcBTC to IntegrationSample...' - ); - await poolMerchant - .connect(operator) - .transferDLCBTC(integrationSample.address, fundAmount); - console.log('Transfer successful'); - console.log('\nšŸ“ˆ Allocating to IntegrationSample...'); const integrationSharesBefore = await mockVault.balanceOf( integrationSample.address ); console.log( - 'IntegrationSample shares before allocation:', + 'IntegrationSample ERC4626 shares before allocation:', integrationSharesBefore.toString() ); await poolMerchant.connect(operator).allocateToIntegration(vaultId); const shares = await poolMerchant.getVaultShares(vaultId); - console.log('Allocated shares:', shares.toString()); + console.log('Allocated shares for vaultID:', shares.toString()); const integrationSharesAfter = await mockVault.balanceOf( integrationSample.address ); console.log( - 'IntegrationSample shares after allocation:', + 'IntegrationSample ERC4626 shares after allocation:', integrationSharesAfter.toString() ); @@ -223,7 +220,6 @@ async function main() { '\nšŸ¦ Performing partial withdrawal to trigger reward harvest...' ); const withdrawAmount = fundAmount.div(2); - const sharesToWithdraw = withdrawAmount; console.log('\nšŸ” Testing withdrawal process...'); @@ -234,44 +230,43 @@ async function main() { console.log(' - Allocated:', vaultBefore.allocated.toString()); console.log(' - Shares:', sharesBefore.toString()); - const assetToken = await ethers.getContractAt( - 'IERC20', - MAINNET_ADDRESSES.DLC_BTC - ); - - const integrationBalance = await assetToken.balanceOf( + const integrationBalance = await dlcBTC.balanceOf( integrationSample.address ); console.log( - 'IntegrationSample asset balance:', + 'IntegrationSample dlcBTC balance:', integrationBalance.toString() ); - const vaultBalance = await assetToken.balanceOf(mockVault.address); - console.log('MockERC4626Vault asset balance:', vaultBalance.toString()); + const vaultBalance = await dlcBTC.balanceOf(mockVault.address); + console.log('MockERC4626Vault dlcBTC balance:', vaultBalance.toString()); console.log( '\nAttempting withdrawal of shares:', - sharesToWithdraw.toString() + withdrawAmount.toString() ); + + const previewRedeem = await mockVault.previewRedeem(withdrawAmount); + console.log('Preview redeem amount:', previewRedeem.toString()); + const maxRedeem = await mockVault.maxRedeem(integrationSample.address); + try { - const integrationAssetBalance = await mockVault.balanceOf( + const integrationSampleSharesBalance = await mockVault.balanceOf( integrationSample.address ); console.log( - 'IntegrationSample asset balance before withdrawal:', - integrationAssetBalance.toString() + 'IntegrationSample ERC4626 shares balance before withdrawal:', + integrationSampleSharesBalance.toString() ); - if (integrationAssetBalance.lt(withdrawAmount)) { + if (integrationSampleSharesBalance.lt(withdrawAmount)) { console.log( 'āš ļø IntegrationSample does not have enough balance to withdraw' ); } - const withdrawTx = await poolMerchant .connect(operator) - .withdrawFromVault(vaultId, sharesToWithdraw, { + .withdrawFromVault(vaultId, withdrawAmount, { gasLimit: 2000000, }); @@ -308,7 +303,7 @@ async function main() { } catch (error) { console.log('\nāŒ Withdrawal failed:', error.message); - const integrationBalanceAfter = await assetToken.balanceOf( + const integrationBalanceAfter = await dlcBTC.balanceOf( integrationSample.address ); console.log( @@ -316,12 +311,149 @@ async function main() { integrationBalanceAfter.toString() ); - const vaultBalanceAfter = await assetToken.balanceOf(mockVault.address); + const vaultBalanceAfter = await dlcBTC.balanceOf(mockVault.address); console.log( 'MockERC4626Vault asset balance after withdrawal attempt:', vaultBalanceAfter.toString() ); } + // First mint some reward tokens to the integration + const rewardAmount = ethers.utils.parseUnits('1000', 8); // 1000 DLC + console.log( + '\nšŸ’° Minting rewards to IntegrationSample:', + rewardAmount.toString() + ); + await dlcBTC.mint(integrationSample.address, rewardAmount); + + // Check initial rewards state + console.log('\nšŸ“Š Checking initial rewards state...'); + const initialState = await integrationSample.getRewardState(); + console.log('Integration reward state:', { + totalShares: initialState.totalShares.toString(), + currentRewardBalance: initialState.currentRewardBalance.toString(), + trackedRewardBalance: initialState.trackedRewardBalance.toString(), + accRewardPerShare: initialState.accRewardPerShare.toString(), + }); + + // Check vault's initial reward state + console.log('\nšŸ“Š Checking vault initial rewards...'); + const [lastClaimedAt, pendingAmount] = await poolMerchant.getVaultReward( + vaultId, + MAINNET_ADDRESSES.DLC_BTC + ); + console.log('Vault reward state:', { + lastClaimedAt: lastClaimedAt.toString(), + pendingAmount: pendingAmount.toString(), + }); + + // Mine blocks to accrue rewards + console.log('\nā³ Mining blocks to accrue rewards...'); + await hre.network.provider.send('hardhat_mine', ['0x100']); + + // Check pending rewards for vault + console.log('\nšŸ” Checking pending integration rewards...'); + const pendingRewards = await integrationSample.getPendingRewards(); + console.log('Pending integration rewards:', pendingRewards[0].toString()); + + // Get vault's allocation details before harvest + console.log('\nšŸ“Š Vault allocation before harvest:'); + const vaultAllocationBefore = + await poolMerchant.getVaultAllocationDetails(vaultId); + console.log({ + valueMinted: vaultAllocationBefore.valueMinted.toString(), + allocated: vaultAllocationBefore.allocated.toString(), + unallocated: vaultAllocationBefore.unallocated.toString(), + }); + + // Check PoolMerchant's DLC balance before harvest + const poolMerchantBalanceBefore = await dlcBTC.balanceOf( + poolMerchant.address + ); + console.log( + '\nšŸ’° PoolMerchant DLC balance before harvest:', + poolMerchantBalanceBefore.toString() + ); + + // Harvest rewards + console.log('\nšŸŒ¾ Harvesting rewards...'); + const harvestTx = await poolMerchant + .connect(harvester) + .harvestRewardsForIntegration(integrationSample.address); + const harvestReceipt = await harvestTx.wait(); + + // Get harvest events + console.log('\nšŸ“œ Checking harvest events...'); + const harvestEvents = harvestReceipt.events.filter( + (e) => e.event === 'RewardsHarvested' + ); + for (const event of harvestEvents) { + console.log('Harvest event:', { + integration: event.args.integration, + rewardToken: event.args.rewardToken, + harvester: event.args.harvester, + amount: event.args.amount.toString(), + }); + } + + // Check final reward states + console.log('\nšŸ“Š Checking final states...'); + + // Integration final state + const finalState = await integrationSample.getRewardState(); + console.log('Integration final state:', { + totalShares: finalState.totalShares.toString(), + currentRewardBalance: finalState.currentRewardBalance.toString(), + trackedRewardBalance: finalState.trackedRewardBalance.toString(), + accRewardPerShare: finalState.accRewardPerShare.toString(), + }); + + // Vault's final reward state + const [finalLastClaimedAt, finalPendingAmount] = + await poolMerchant.getVaultReward(vaultId, MAINNET_ADDRESSES.DLC_BTC); + console.log('Vault final reward state:', { + lastClaimedAt: finalLastClaimedAt.toString(), + pendingAmount: finalPendingAmount.toString(), + }); + + // PoolMerchant's final balance + const poolMerchantBalanceAfter = await dlcBTC.balanceOf( + poolMerchant.address + ); + console.log( + '\nšŸ’° PoolMerchant DLC balance after harvest:', + poolMerchantBalanceAfter.toString() + ); + + // Check if rewards can be claimed + console.log('\nšŸŽÆ Attempting to claim rewards...'); + try { + const claimTx = await poolMerchant + .connect(operator) + .claimRewards(vaultId, MAINNET_ADDRESSES.DLC_BTC); + const claimReceipt = await claimTx.wait(); + + const claimEvents = claimReceipt.events.filter( + (e) => e.event === 'RewardsClaimed' + ); + for (const event of claimEvents) { + console.log('Claim event:', { + uuid: event.args.uuid, + rewardToken: event.args.rewardToken, + amount: event.args.amount.toString(), + }); + } + } catch (error) { + console.log('āŒ Claim failed:', error.message); + } + + // Final balance check + console.log('\nšŸ“Š Final balance check:'); + const finalBalances = { + poolMerchant: await dlcBTC.balanceOf(poolMerchant.address), + integration: await dlcBTC.balanceOf(integrationSample.address), + vault: await dlcBTC.balanceOf(mockVault.address), + }; + console.log(finalBalances); } main() From 6eb314977e48a14c926aa6f2a5890bc7cba8c833 Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 7 Nov 2024 12:15:15 +0100 Subject: [PATCH 17/27] feat: taproot-integration --> uuid --- contracts/PoolMerchant.sol | 160 ++++++++-- scripts/pool_merchant_flow.js | 284 ------------------ ...ol_merchant_flow_with_integrationSample.js | 72 +++-- scripts/pool_merchant_setup_flow.js | 98 ++++++ test/PoolMerchant.test.js | 140 +++++---- 5 files changed, 363 insertions(+), 391 deletions(-) delete mode 100644 scripts/pool_merchant_flow.js create mode 100644 scripts/pool_merchant_setup_flow.js diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 4ccf78b..b7ad800 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -50,7 +50,6 @@ contract PoolMerchant is //////////////////////////////////////////////////////////////// bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant HARVESTER_ROLE = keccak256("HARVESTER_ROLE"); IDLCManager public dlcManager; @@ -83,9 +82,10 @@ contract PoolMerchant is } mapping(bytes32 => VaultInfo) internal _vaults; - mapping(string => bytes32[]) public vaultsByBitcoinAddress; + mapping(string => bytes32[]) public vaultsByTaprootPubKey; mapping(address => Integration) public integrations; mapping(address => RewardToken) public rewardTokens; + mapping(bytes32 => bytes32) public uuidByTaprootAndIntegration; // taproot-integration -> uuid address[] public activeIntegrations; uint256[50] private __gap; @@ -97,7 +97,7 @@ contract PoolMerchant is event PendingVaultCreated( bytes32 indexed uuid, string taprootPubKey, - string withdrawalTxId, + string wdPSBT, address integration ); event VaultWithdrawn(bytes32 indexed uuid, uint256 amount); @@ -152,7 +152,7 @@ contract PoolMerchant is function createPendingVault( string calldata taprootPubKey, - string calldata withdrawalTxId, + string calldata wdPSBT, address integration ) external @@ -163,35 +163,36 @@ contract PoolMerchant is { require(integrations[integration].isActive, "Integration not active"); - bytes32 _uuid = dlcManager.setupPendingVault( - taprootPubKey, - withdrawalTxId + bytes32 mappingKey = _createMappingKey(taprootPubKey, integration); + require( + uuidByTaprootAndIntegration[mappingKey] == bytes32(0), + "Vault already exists for this taproot-integration pair" ); + bytes32 _uuid = dlcManager.setupPendingVault(taprootPubKey, wdPSBT); + _vaults[_uuid].integration = integration; _addVaultToIntegration(_uuid, integration); - vaultsByBitcoinAddress[taprootPubKey].push(_uuid); + vaultsByTaprootPubKey[taprootPubKey].push(_uuid); + uuidByTaprootAndIntegration[mappingKey] = _uuid; - emit PendingVaultCreated( - _uuid, - taprootPubKey, - withdrawalTxId, - integration - ); + emit PendingVaultCreated(_uuid, taprootPubKey, wdPSBT, integration); return _uuid; } function withdrawFromVault( - bytes32 uuid, + string calldata taprootPubKey, + address integration, uint256 amount ) external onlyRole(ATTESTOR_ROLE) nonReentrant whenNotPaused { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); require(dlc.uuid != bytes32(0), "Vault does not exist"); VaultInfo storage vault = _vaults[uuid]; - address integration = vault.integration; - require(integration != address(0), "No integration set"); require(amount <= vault.allocated, "Amount exceeds allocation"); Integration storage integ = integrations[integration]; @@ -280,9 +281,13 @@ contract PoolMerchant is // TODO: add auth/a way for users to claim their rewards // So, it would not be msg.sender who gets this function claimRewards( - bytes32 uuid, + string calldata taprootPubKey, + address integration, address rewardToken ) external nonReentrant whenNotPaused { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; VaultInfo storage vault = _vaults[uuid]; UserReward storage reward = vault.rewards[rewardToken]; require(reward.pendingAmount > 0, "No rewards to claim"); @@ -333,11 +338,13 @@ contract PoolMerchant is // Allocate dlcBTC to the vault's integration function allocateToIntegration( - bytes32 uuid - ) external onlyRole(OPERATOR_ROLE) nonReentrant whenNotPaused { + string calldata taprootPubKey, + address integration + ) external onlyRole(ATTESTOR_ROLE) nonReentrant whenNotPaused { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; VaultInfo storage vault = _vaults[uuid]; - address integration = vault.integration; - require(integration != address(0), "No integration set"); require(integrations[integration].isActive, "Integration not active"); DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); @@ -424,6 +431,16 @@ contract PoolMerchant is return integrations[integration].vaults; } + function getVaultByTaprootAndIntegration( + string calldata taprootPubKey, + address integration + ) external view returns (bytes32) { + return + uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; + } + //////////////////////////////////////////////////////////////// // VAULT FUNCTIONS // //////////////////////////////////////////////////////////////// @@ -432,10 +449,34 @@ contract PoolMerchant is return _vaults[uuid].shares; } + function getVaultSharesByTaprootAndIntegration( + string calldata taprootPubKey, + address integration + ) external view returns (uint256) { + return + _vaults[ + uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ] + ].shares; + } + function getVaultIntegration(bytes32 uuid) external view returns (address) { return _vaults[uuid].integration; } + function getVaultIntegrationByTaprootAndIntegration( + string calldata taprootPubKey, + address integration + ) external view returns (address) { + return + _vaults[ + uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ] + ].integration; + } + function getVaultReward( bytes32 uuid, address rewardToken @@ -444,10 +485,23 @@ contract PoolMerchant is return (reward.lastClaimedAt, reward.pendingAmount); } + function getVaultRewardByTaprootAndIntegration( + string calldata taprootPubKey, + address integration, + address rewardToken + ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { + UserReward storage reward = _vaults[ + uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ] + ].rewards[rewardToken]; + return (reward.lastClaimedAt, reward.pendingAmount); + } + function getVaultAllocationDetails( bytes32 uuid ) - external + public view returns (uint256 valueMinted, uint256 allocated, uint256 unallocated) { @@ -462,6 +516,20 @@ contract PoolMerchant is ); } + function getVaultAllocationDetailsByTaprootAndIntegration( + string calldata taprootPubKey, + address integration + ) + external + view + returns (uint256 valueMinted, uint256 allocated, uint256 unallocated) + { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; + return getVaultAllocationDetails(uuid); + } + function getUnallocatedAmount(bytes32 uuid) public view returns (uint256) { DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); return @@ -471,6 +539,16 @@ contract PoolMerchant is ); } + function getUnallocatedAmountByTaprootAndIntegration( + string calldata taprootPubKey, + address integration + ) external view returns (uint256) { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, integration) + ]; + return getUnallocatedAmount(uuid); + } + function getPendingIntegrationRewards( address integration ) @@ -490,10 +568,10 @@ contract PoolMerchant is //////////////////////////////////////////////////////////////// // Get all vault UUIDs for a taproot public key - function getVaultsByBitcoinAddress( + function getVaultsByTaprootPubKey( string calldata taprootPubKey ) external view returns (bytes32[] memory) { - return vaultsByBitcoinAddress[taprootPubKey]; + return vaultsByTaprootPubKey[taprootPubKey]; } // Get details for a specific vault by UUID @@ -501,7 +579,7 @@ contract PoolMerchant is bytes32 uuid, address[] calldata _rewardTokens ) - external + public view returns ( address integration, @@ -536,6 +614,29 @@ contract PoolMerchant is } } + function getVaultDetailsByTaprootAndIntegration( + string calldata taprootPubKey, + address _integration, + address[] calldata _rewardTokens + ) + external + view + returns ( + address integration, + uint256 shares, + uint256 valueMinted, + uint256 allocated, + uint256 unallocated, + uint256[] memory lastClaimedAt, + uint256[] memory pendingAmounts + ) + { + bytes32 uuid = uuidByTaprootAndIntegration[ + _createMappingKey(taprootPubKey, _integration) + ]; + return getVaultDetails(uuid, _rewardTokens); + } + //////////////////////////////////////////////////////////////// // ADMIN FUNCTIONS // //////////////////////////////////////////////////////////////// @@ -569,6 +670,13 @@ contract PoolMerchant is // UTILITIES // //////////////////////////////////////////////////////////////// + function _createMappingKey( + string memory taprootPubKey, + address integration + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(taprootPubKey, integration)); + } + // Required interface implementations function onERC721Received( address, diff --git a/scripts/pool_merchant_flow.js b/scripts/pool_merchant_flow.js deleted file mode 100644 index 5fabc9b..0000000 --- a/scripts/pool_merchant_flow.js +++ /dev/null @@ -1,284 +0,0 @@ -const hre = require('hardhat'); -const { ethers, upgrades } = require('hardhat'); - -const { getSignatures, whitelistAddress } = require('../test/utils'); - -async function fundAccount(address) { - await hre.network.provider.send('hardhat_setBalance', [ - address, - '0x2000000000000000000', // 2 ETH - ]); -} - -async function main() { - // Compile contracts - console.log('\nšŸ”Ø Compiling contracts...'); - await hre.run('compile'); - - console.log('\nšŸš€ Starting happy path integration test...'); - - // Configure network and addresses - const MAINNET_ADDRESSES = { - DLC_MANAGER: '0x20157DBAbb84e3BBFE68C349d0d44E48AE7B5AD2', - DLC_BTC: '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', - CURVE_POOL: '0xe957cE03cCdd88f02ed8b05C9a3A28ABEf38514A', - CURVE_GAUGE: '0x02b8e750E68cb648dB2c2ac4BBb47A10A5c12588', - CRV_TOKEN: '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978', - }; - - // Impersonate necessary accounts - const accounts = await ethers.getSigners(); - const deployer = accounts[0]; - const dlcAdmin = await ethers.getImpersonatedSigner( - '0xaA2949C5285C2f2887ABD567865344240c29d619' - ); - const dlcCritical = await ethers.getImpersonatedSigner( - '0x24f75096ad315Ab617a3d0f2621aC3e9D391Aa77' - ); - const attestor_1 = await ethers.getImpersonatedSigner( - '0x989E9c4005ABc2a8E4b85544B44d2d95cfDe08de' - ); - const attestor_2 = await ethers.getImpersonatedSigner( - '0xBe4aAE47A62f67bdF93eA9f5F189ae51B1b54492' - ); - const attestor_3 = await ethers.getImpersonatedSigner( - '0x7B254D8C6eBd9662A52180B06920aEA4f23a8940' - ); - const attestor_4 = await ethers.getImpersonatedSigner( - '0x194c697e8343EaB3C53917BA7e597d02687f8BA0' - ); - const attestor_5 = await ethers.getImpersonatedSigner( - '0x2daef70747eb9E97E5f31A9EBDbda593918F8bE7' - ); - const operator = deployer; - const harvester = deployer; - - const attestors = [ - attestor_1, - attestor_2, - attestor_3, - attestor_4, - attestor_5, - ]; - - console.log('\nšŸ’° Funding impersonated accounts...'); - for (const account of [dlcAdmin, ...attestors]) { - await fundAccount(account.address); - } - - // DLCManager upgrade remains the same - console.log('\nšŸ”„ Upgrading DLCManager...'); - const proxyAdminAddress = await upgrades.erc1967.getAdminAddress( - MAINNET_ADDRESSES.DLC_MANAGER - ); - console.log('Proxy admin address:', proxyAdminAddress); - const connectedProxyAdmin = new ethers.Contract( - proxyAdminAddress, - [ - 'function owner() view returns (address)', - 'function upgrade(address, address) external', - ], - dlcCritical - ); - - const DLCManager = await ethers.getContractFactory('DLCManager'); - const dlcManagerImpl = await DLCManager.deploy(); - await dlcManagerImpl.deployed(); - - await connectedProxyAdmin.upgrade( - MAINNET_ADDRESSES.DLC_MANAGER, - dlcManagerImpl.address - ); - - console.log('DLCManager upgraded'); - - console.log('\nšŸ“ Getting contract instances...'); - const dlcManager = await ethers.getContractAt( - 'DLCManager', - MAINNET_ADDRESSES.DLC_MANAGER - ); - const dlcBTC = await ethers.getContractAt( - 'DLCBTC', - MAINNET_ADDRESSES.DLC_BTC - ); - - await dlcManager.connect(dlcAdmin).setSkipSignatureVerification(true); - - // Deploy PoolMerchant - console.log('\nšŸ—ļø Deploying PoolMerchant...'); - const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); - const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ - MAINNET_ADDRESSES.DLC_MANAGER, - MAINNET_ADDRESSES.DLC_BTC, - deployer.address, - ]); - await poolMerchant.deployed(); - console.log('PoolMerchant deployed to:', poolMerchant.address); - - // Whitelist PoolMerchant - await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); - - // Deploy CurveIntegration - console.log('\nšŸ—ļø Deploying CurveIntegration...'); - const CurveIntegration = - await ethers.getContractFactory('CurveIntegration'); - const curveIntegration = await CurveIntegration.deploy( - MAINNET_ADDRESSES.CURVE_POOL, - MAINNET_ADDRESSES.CURVE_GAUGE, - poolMerchant.address, - MAINNET_ADDRESSES.DLC_BTC - ); - await curveIntegration.deployed(); - console.log('CurveIntegration deployed to:', curveIntegration.address); - - // Setup roles and integration - console.log('\nšŸ”‘ Setting up roles and integration...'); - await poolMerchant.grantRole( - await poolMerchant.ATTESTOR_ROLE(), - attestor_1.address - ); - await poolMerchant.grantRole( - await poolMerchant.OPERATOR_ROLE(), - operator.address - ); - await poolMerchant.grantRole( - await poolMerchant.HARVESTER_ROLE(), - harvester.address - ); - - console.log('\nšŸŖ™ Adding CRV as reward token...'); - await poolMerchant.addRewardToken(MAINNET_ADDRESSES.CRV_TOKEN); - - console.log('\nšŸ”„ Setting up CurveIntegration...'); - await poolMerchant.setIntegration(curveIntegration.address, [ - MAINNET_ADDRESSES.CRV_TOKEN, - ]); - - // Create vault with integration - console.log('\nšŸ“¦ Creating vault with Curve integration...'); - const mockBtcTxId = '0x123'; - const mockTaprootPubkey = '0x12345'; - - const tx = await poolMerchant - .connect(attestor_1) - .createPendingVault( - mockTaprootPubkey, - mockBtcTxId, - curveIntegration.address, - { - gasLimit: 1000000, - } - ); - - const receipt = await tx.wait(); - const vaultId = receipt.events.find( - (e) => e.event === 'PendingVaultCreated' - ).args.uuid; - console.log('Vault created with ID:', vaultId); - - // Fund vault - console.log('\nšŸ’° Funding vault...'); - const fundAmount = ethers.utils.parseUnits('1', 8); // 1 BTC - console.log('Funding amount:', fundAmount.toString()); - - const tx3 = await dlcManager - .connect(attestor_1) - .setStatusFunded(vaultId, mockBtcTxId, [], fundAmount); - await tx3.wait(); - - // Allocate to Curve - console.log('\nšŸ“ˆ Allocating to Curve...'); - await poolMerchant.connect(operator).allocateToIntegration(vaultId); - - const shares = await poolMerchant.getVaultShares(vaultId); - console.log('Allocated shares:', shares.toString()); - - // Mine blocks and check initial state - console.log('\nā³ Mining blocks to accrue rewards...'); - await hre.network.provider.send('hardhat_mine', ['0x100']); // Mine 256 blocks - - // Perform partial withdrawal to trigger reward harvest - console.log( - '\nšŸ¦ Performing partial withdrawal to trigger reward harvest...' - ); - const withdrawAmount = fundAmount.div(2); - - console.log('\nšŸ” Testing withdrawal process...'); - - // Step 1: Check initial state - const vaultBefore = await poolMerchant.getVaultAllocationDetails(vaultId); - const sharesBefore = await poolMerchant.getVaultShares(vaultId); - console.log('\nInitial state:'); - console.log(' - Total minted:', vaultBefore.valueMinted.toString()); - console.log(' - Allocated:', vaultBefore.allocated.toString()); - console.log(' - Shares:', sharesBefore.toString()); - - // Step 2: Perform withdrawal - console.log('\nAttempting withdrawal of:', withdrawAmount.toString()); - - try { - // First just try the withdrawal - const withdrawTx = await poolMerchant - .connect(attestor_1) - .withdrawFromVault(vaultId, withdrawAmount, { - gasLimit: 2000000, - }); - - await withdrawTx.wait(); - console.log('Withdrawal successful!'); - - // Check post-withdrawal state - const vaultAfter = - await poolMerchant.getVaultAllocationDetails(vaultId); - const sharesAfter = await poolMerchant.getVaultShares(vaultId); - console.log('\nPost-withdrawal state:'); - console.log(' - Total minted:', vaultAfter.valueMinted.toString()); - console.log(' - Allocated:', vaultAfter.allocated.toString()); - console.log(' - Shares:', sharesAfter.toString()); - - // Step 3: Check DLC BTC balances - const dlcBTCBalance = await dlcBTC.balanceOf(poolMerchant.address); - console.log('\nDLC BTC balances:'); - console.log(' - PoolMerchant:', dlcBTCBalance.toString()); - - // Step 4: Separately check for rewards - console.log('\nšŸŒ¾ Checking reward state...'); - const [lastClaimed, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - MAINNET_ADDRESSES.CRV_TOKEN - ); - console.log('Current reward state:'); - console.log( - ' - Last claimed:', - new Date(lastClaimed * 1000).toISOString() - ); - console.log( - ' - Pending amount:', - ethers.utils.formatEther(pendingAmount) - ); - - // Step 5: Try harvesting rewards separately - console.log('\nTrying manual reward harvest...'); - try { - await poolMerchant - .connect(harvester) - .harvestRewardsForIntegration(curveIntegration.address, { - gasLimit: 2000000, - }); - console.log('Manual harvest successful'); - } catch (harvestError) { - console.log('Manual harvest failed:', harvestError.message); - } - } catch (error) { - console.log('\nāŒ Initial withdrawal failed:', error.message); - } - - console.log('\nāœ… Test sequence complete'); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/scripts/pool_merchant_flow_with_integrationSample.js b/scripts/pool_merchant_flow_with_integrationSample.js index 70527df..ed0409b 100644 --- a/scripts/pool_merchant_flow_with_integrationSample.js +++ b/scripts/pool_merchant_flow_with_integrationSample.js @@ -143,10 +143,6 @@ async function main() { await poolMerchant.ATTESTOR_ROLE(), operator.address ); - await poolMerchant.grantRole( - await poolMerchant.OPERATOR_ROLE(), - operator.address - ); await poolMerchant.grantRole( await poolMerchant.HARVESTER_ROLE(), harvester.address @@ -176,9 +172,10 @@ async function main() { ); const receipt = await tx.wait(); - const vaultId = receipt.events.find( - (e) => e.event === 'PendingVaultCreated' - ).args.uuid; + const vaultId = await poolMerchant.getVaultByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); console.log('Vault created with ID:', vaultId); console.log('\nšŸ’° Funding vault...'); @@ -200,10 +197,15 @@ async function main() { integrationSharesBefore.toString() ); - await poolMerchant.connect(operator).allocateToIntegration(vaultId); + await poolMerchant + .connect(operator) + .allocateToIntegration(mockTaprootPubkey, integrationSample.address); - const shares = await poolMerchant.getVaultShares(vaultId); - console.log('Allocated shares for vaultID:', shares.toString()); + const shares = await poolMerchant.getVaultSharesByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); + console.log('Allocated shares for vault:', shares.toString()); const integrationSharesAfter = await mockVault.balanceOf( integrationSample.address @@ -223,8 +225,16 @@ async function main() { console.log('\nšŸ” Testing withdrawal process...'); - const vaultBefore = await poolMerchant.getVaultAllocationDetails(vaultId); - const sharesBefore = await poolMerchant.getVaultShares(vaultId); + const vaultBefore = + await poolMerchant.getVaultAllocationDetailsByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); + const sharesBefore = + await poolMerchant.getVaultSharesByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); console.log('\nInitial state:'); console.log(' - Total minted:', vaultBefore.valueMinted.toString()); console.log(' - Allocated:', vaultBefore.allocated.toString()); @@ -266,16 +276,28 @@ async function main() { } const withdrawTx = await poolMerchant .connect(operator) - .withdrawFromVault(vaultId, withdrawAmount, { - gasLimit: 2000000, - }); + .withdrawFromVault( + mockTaprootPubkey, + integrationSample.address, + withdrawAmount, + { + gasLimit: 2000000, + } + ); await withdrawTx.wait(); console.log('Withdrawal successful!'); const vaultAfter = - await poolMerchant.getVaultAllocationDetails(vaultId); - const sharesAfter = await poolMerchant.getVaultShares(vaultId); + await poolMerchant.getVaultAllocationDetailsByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); + const sharesAfter = + await poolMerchant.getVaultSharesByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address + ); console.log('\nPost-withdrawal state:'); console.log(' - Total minted:', vaultAfter.valueMinted.toString()); console.log(' - Allocated:', vaultAfter.allocated.toString()); @@ -337,10 +359,12 @@ async function main() { // Check vault's initial reward state console.log('\nšŸ“Š Checking vault initial rewards...'); - const [lastClaimedAt, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - MAINNET_ADDRESSES.DLC_BTC - ); + const [lastClaimedAt, pendingAmount] = + await poolMerchant.getVaultRewardByTaprootAndIntegration( + mockTaprootPubkey, + integrationSample.address, + MAINNET_ADDRESSES.DLC_BTC + ); console.log('Vault reward state:', { lastClaimedAt: lastClaimedAt.toString(), pendingAmount: pendingAmount.toString(), @@ -429,7 +453,11 @@ async function main() { try { const claimTx = await poolMerchant .connect(operator) - .claimRewards(vaultId, MAINNET_ADDRESSES.DLC_BTC); + .claimRewards( + mockTaprootPubkey, + integrationSample.address, + MAINNET_ADDRESSES.DLC_BTC + ); const claimReceipt = await claimTx.wait(); const claimEvents = claimReceipt.events.filter( diff --git a/scripts/pool_merchant_setup_flow.js b/scripts/pool_merchant_setup_flow.js new file mode 100644 index 0000000..b8c71ed --- /dev/null +++ b/scripts/pool_merchant_setup_flow.js @@ -0,0 +1,98 @@ +const hre = require('hardhat'); +const { ethers, upgrades } = require('hardhat'); +const { loadContractAddress } = require('./helpers/utils'); + +async function main() { + // Compile contracts + console.log('\nšŸ”Ø Compiling contracts...'); + await hre.run('compile'); + + console.log('\nšŸš€ Starting setup...'); + + const accounts = await ethers.getSigners(); + const deployer = accounts[0]; + const dlcAdmin = deployer; + const attestor_1 = ''; // TODO: + const operator = deployer; + const harvester = deployer; + + // DLCManager upgrade remains the same + console.log('\nšŸ”„ Upgrading DLCManager...'); + const network = hre.network.name; + const proxyAddress = await loadContractAddress('DLCManager', network); + const newImplementation = await ethers.getContractFactory('DLCManager'); + await upgrades.upgradeProxy(proxyAddress, newImplementation); + + console.log('DLCManager upgraded'); + + console.log('\nšŸ“ Getting contract instances...'); + const dlcManager = await ethers.getContractAt('DLCManager', proxyAddress); + + const dlcBTCAddress = await loadContractAddress('DLCBTC', network); + const dlcBTC = await ethers.getContractAt('DLCBTC', dlcBTCAddress); + + // Deploy PoolMerchant + console.log('\nšŸ—ļø Deploying PoolMerchant...'); + const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + proxyAddress, + dlcBTCAddress, + deployer.address, + ]); + await poolMerchant.deployed(); + console.log('PoolMerchant deployed to:', poolMerchant.address); + + // Whitelist PoolMerchant + await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); + + console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); + const MockERC4626Vault = + await ethers.getContractFactory('MockERC4626Vault'); + const mockVault = await MockERC4626Vault.deploy(dlcBTCAddress); + await mockVault.deployed(); + console.log('MockERC4626Vault deployed to:', mockVault.address); + + console.log('\nšŸ—ļø Deploying IntegrationSample...'); + const IntegrationSample = + await ethers.getContractFactory('IntegrationSample'); + + const rewardRatePerSecond = ethers.BigNumber.from('317'); + + const integrationSample = await IntegrationSample.deploy( + mockVault.address, + dlcBTCAddress, + rewardRatePerSecond, + poolMerchant.address, + dlcBTC.address + ); + await integrationSample.deployed(); + console.log('IntegrationSample deployed to:', integrationSample.address); + + // Setup roles and integration + console.log('\nšŸ”‘ Setting up roles and integration...'); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + attestor_1 + ); + await poolMerchant.grantRole( + await poolMerchant.HARVESTER_ROLE(), + harvester.address + ); + + console.log('\nšŸŖ™ Adding dlcBTC as reward token...'); + await poolMerchant.addRewardToken(dlcBTCAddress); + + console.log('\nšŸ”„ Setting up Integration...'); + await poolMerchant.setIntegration(integrationSample.address, [ + dlcBTCAddress, + ]); + + console.log('\nāœ… Setup sequence complete'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/PoolMerchant.test.js b/test/PoolMerchant.test.js index 86a75ec..40df329 100644 --- a/test/PoolMerchant.test.js +++ b/test/PoolMerchant.test.js @@ -14,26 +14,21 @@ describe('PoolMerchant', () => { let poolMerchant; let dlcManager; let dlcBtc; - let curveIntegration; - let curvePool; - let curveGauge; let deployer; let attestor_1; let attestor_2; let attestor_3; let attestors; - let operator; let harvester; let user; let mockIntegration; let mockRewardToken; + const mockTaprootPubkey = 'taproot123'; + const mockWithdrawalTxId = 'tx123'; const ATTESTOR_ROLE = ethers.utils.keccak256( ethers.utils.toUtf8Bytes('ATTESTOR_ROLE') ); - const OPERATOR_ROLE = ethers.utils.keccak256( - ethers.utils.toUtf8Bytes('OPERATOR_ROLE') - ); const HARVESTER_ROLE = ethers.utils.keccak256( ethers.utils.toUtf8Bytes('HARVESTER_ROLE') ); @@ -41,15 +36,8 @@ describe('PoolMerchant', () => { let btcFeeRecipient = '0x000001'; beforeEach(async function () { - [ - deployer, - attestor_1, - attestor_2, - attestor_3, - operator, - harvester, - user, - ] = await ethers.getSigners(); + [deployer, attestor_1, attestor_2, attestor_3, harvester, user] = + await ethers.getSigners(); attestors = [attestor_1, attestor_2, attestor_3]; @@ -92,13 +80,9 @@ describe('PoolMerchant', () => { // Setup roles await poolMerchant.grantRole(ATTESTOR_ROLE, attestor_1.address); - await poolMerchant.grantRole(OPERATOR_ROLE, operator.address); await poolMerchant.grantRole(HARVESTER_ROLE, harvester.address); await whitelistAddress(dlcManager, poolMerchant); - - // Transfer CurveIntegration ownership to PoolMerchant - // await curveIntegration.transferOwnership(poolMerchant.address); }); describe('Initialization', function () { @@ -110,8 +94,6 @@ describe('PoolMerchant', () => { expect( await poolMerchant.hasRole(ATTESTOR_ROLE, attestor_1.address) ).to.be.true; - expect(await poolMerchant.hasRole(OPERATOR_ROLE, operator.address)) - .to.be.true; expect( await poolMerchant.hasRole(HARVESTER_ROLE, harvester.address) ).to.be.true; @@ -120,7 +102,6 @@ describe('PoolMerchant', () => { describe('Vault Creation and Integration', function () { beforeEach(async function () { - // Setup integration await poolMerchant .connect(deployer) .addRewardToken(mockRewardToken.address); @@ -132,34 +113,37 @@ describe('PoolMerchant', () => { }); it('should create a pending vault with integration', async function () { - const taprootPubKey = 'taproot123'; - const withdrawalTxId = 'tx123'; - const tx = await poolMerchant .connect(attestor_1) .createPendingVault( - taprootPubKey, - withdrawalTxId, + mockTaprootPubkey, + mockWithdrawalTxId, mockIntegration.address ); + const uuid = await getEventArg(tx, 'PendingVaultCreated', 'uuid'); + await expect(tx) .to.emit(poolMerchant, 'PendingVaultCreated') .withArgs( - await getEventArg(tx, 'PendingVaultCreated', 'uuid'), - taprootPubKey, - withdrawalTxId, + uuid, + mockTaprootPubkey, + mockWithdrawalTxId, mockIntegration.address ); - const vaultId = await getEventArg( - tx, - 'PendingVaultCreated', - 'uuid' - ); - expect(await poolMerchant.getVaultIntegration(vaultId)).to.equal( + const vaultId = await poolMerchant.getVaultByTaprootAndIntegration( + mockTaprootPubkey, mockIntegration.address ); + expect(vaultId).to.equal(uuid); + + expect( + await poolMerchant.getVaultIntegrationByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address + ) + ).to.equal(mockIntegration.address); }); it('should not create vault with inactive integration', async function () { @@ -173,8 +157,8 @@ describe('PoolMerchant', () => { poolMerchant .connect(attestor_1) .createPendingVault( - 'taproot123', - 'tx123', + mockTaprootPubkey, + mockWithdrawalTxId, newIntegration.address ) ).to.be.revertedWith('Integration not active'); @@ -266,7 +250,6 @@ describe('PoolMerchant', () => { }); describe('Allocation and Rewards', function () { - let vaultId; const initialFunding = ethers.utils.parseUnits('1', 8); beforeEach(async function () { @@ -284,17 +267,22 @@ describe('PoolMerchant', () => { const tx = await poolMerchant .connect(attestor_1) .createPendingVault( - 'taproot123', - 'tx123', + mockTaprootPubkey, + mockWithdrawalTxId, mockIntegration.address ); - vaultId = await getEventArg(tx, 'PendingVaultCreated', 'uuid'); + + const vaultId = await getEventArg( + tx, + 'PendingVaultCreated', + 'uuid' + ); // Fund vault const signatureBytesForFunding = await getSignatures( { uuid: vaultId, - btcTxId: 'tx123', + btcTxId: mockWithdrawalTxId, functionString: 'set-status-funded', newLockedAmount: initialFunding, }, @@ -305,45 +293,73 @@ describe('PoolMerchant', () => { .connect(attestor_1) .setStatusFunded( vaultId, - 'tx123', + mockWithdrawalTxId, signatureBytesForFunding, initialFunding ); }); it('should allocate to integration', async function () { + const vaultId = await poolMerchant.getVaultByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address + ); + await expect( - poolMerchant.connect(operator).allocateToIntegration(vaultId) + poolMerchant + .connect(attestor_1) + .allocateToIntegration( + mockTaprootPubkey, + mockIntegration.address + ) ) .to.emit(poolMerchant, 'SharesAllocated') - .withArgs(vaultId, mockIntegration.address, initialFunding); // Assuming 1:1 share ratio + .withArgs(vaultId, mockIntegration.address, initialFunding); - const shares = await poolMerchant.getVaultShares(vaultId); + const shares = + await poolMerchant.getVaultSharesByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address + ); expect(shares).to.equal(initialFunding); const details = - await poolMerchant.getVaultAllocationDetails(vaultId); + await poolMerchant.getVaultAllocationDetailsByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address + ); expect(details.allocated).to.equal(initialFunding); expect(details.unallocated).to.equal(0); }); it('should harvest rewards during withdrawal', async function () { // First allocate - await poolMerchant.connect(operator).allocateToIntegration(vaultId); + await poolMerchant + .connect(attestor_1) + .allocateToIntegration( + mockTaprootPubkey, + mockIntegration.address + ); // Mock some rewards const rewardAmount = ethers.utils.parseEther('10'); await mockRewardToken.mint(mockIntegration.address, rewardAmount); await mockIntegration.mockRewards([rewardAmount]); - // Withdraw partial amount const withdrawAmount = initialFunding.div(2); + const vaultId = await poolMerchant.getVaultByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address + ); - // Should emit both rewards harvested and withdrawal events await expect( poolMerchant .connect(attestor_1) - .withdrawFromVault(vaultId, withdrawAmount) + .withdrawFromVault( + mockTaprootPubkey, + mockIntegration.address, + withdrawAmount + ) ) .to.emit(poolMerchant, 'RewardsHarvested') .withArgs( @@ -355,16 +371,22 @@ describe('PoolMerchant', () => { .and.to.emit(poolMerchant, 'VaultWithdrawn') .withArgs(vaultId, withdrawAmount); - // Check rewards were distributed - const [_, pendingAmount] = await poolMerchant.getVaultReward( - vaultId, - mockRewardToken.address - ); + const [_, pendingAmount] = + await poolMerchant.getVaultRewardByTaprootAndIntegration( + mockTaprootPubkey, + mockIntegration.address, + mockRewardToken.address + ); expect(pendingAmount).to.equal(rewardAmount); }); it('should still allow manual reward harvesting by harvester', async function () { - await poolMerchant.connect(operator).allocateToIntegration(vaultId); + await poolMerchant + .connect(attestor_1) + .allocateToIntegration( + mockTaprootPubkey, + mockIntegration.address + ); const rewardAmount = ethers.utils.parseEther('10'); await mockRewardToken.mint(mockIntegration.address, rewardAmount); From eaad16c10a8f6d2f2bb43ad25f68ee3aa5f8ad9b Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 7 Nov 2024 13:56:08 +0100 Subject: [PATCH 18/27] feat: getvaultDetails only takes a uuid --- contracts/PoolMerchant.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index b7ad800..b8816e1 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -576,8 +576,7 @@ contract PoolMerchant is // Get details for a specific vault by UUID function getVaultDetails( - bytes32 uuid, - address[] calldata _rewardTokens + bytes32 uuid ) public view @@ -596,12 +595,15 @@ contract PoolMerchant is require(vault.integration != address(0), "Vault not found"); DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); + Integration storage integ = integrations[vault.integration]; integration = vault.integration; shares = vault.shares; valueMinted = dlc.valueMinted; allocated = vault.allocated; unallocated = valueMinted - allocated; + // Get reward tokens from the integration strategy + address[] memory _rewardTokens = integ.strategy.getRewardTokens(); // Get reward data lastClaimedAt = new uint256[](_rewardTokens.length); @@ -616,8 +618,7 @@ contract PoolMerchant is function getVaultDetailsByTaprootAndIntegration( string calldata taprootPubKey, - address _integration, - address[] calldata _rewardTokens + address _integration ) external view @@ -634,7 +635,7 @@ contract PoolMerchant is bytes32 uuid = uuidByTaprootAndIntegration[ _createMappingKey(taprootPubKey, _integration) ]; - return getVaultDetails(uuid, _rewardTokens); + return getVaultDetails(uuid); } //////////////////////////////////////////////////////////////// From c00f5a41261afdaa8c3b969033908e43e827e588 Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 7 Nov 2024 15:50:42 +0100 Subject: [PATCH 19/27] ref: update vaultDetails and reward naming --- contracts/PoolMerchant.sol | 126 +++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 46 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index b8816e1..d7ecab1 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -62,7 +62,7 @@ contract PoolMerchant is struct UserReward { uint256 lastClaimedAt; - uint256 pendingAmount; + uint256 harvestedAmount; } struct VaultInfo { @@ -73,6 +73,18 @@ contract PoolMerchant is uint256 integrationIndex; // Index in the integration's vault array + 1 (0 means not in array) } + struct VaultDetails { + address integration; + uint256 shares; + uint256 valueMinted; + uint256 allocated; + uint256 unallocated; + address[] rewardTokens; + uint256[] lastClaimedAt; + uint256[] harvestedRewards; // Already harvested rewards + uint256[] pendingRewards; // Rewards that can be harvested now + } + struct Integration { IIntegration strategy; bool isActive; @@ -252,7 +264,9 @@ contract PoolMerchant is uint256 vaultReward = amount.mul(vault.shares).div( integ.totalShares ); - reward.pendingAmount = reward.pendingAmount.add(vaultReward); + reward.harvestedAmount = reward.harvestedAmount.add( + vaultReward + ); emit RewardsHarvested( integration, @@ -290,10 +304,10 @@ contract PoolMerchant is ]; VaultInfo storage vault = _vaults[uuid]; UserReward storage reward = vault.rewards[rewardToken]; - require(reward.pendingAmount > 0, "No rewards to claim"); + require(reward.harvestedAmount > 0, "No rewards to claim"); - uint256 amount = reward.pendingAmount; - reward.pendingAmount = 0; + uint256 amount = reward.harvestedAmount; + reward.harvestedAmount = 0; reward.lastClaimedAt = block.timestamp; require( @@ -480,22 +494,22 @@ contract PoolMerchant is function getVaultReward( bytes32 uuid, address rewardToken - ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { + ) external view returns (uint256 lastClaimedAt, uint256 harvestedAmount) { UserReward storage reward = _vaults[uuid].rewards[rewardToken]; - return (reward.lastClaimedAt, reward.pendingAmount); + return (reward.lastClaimedAt, reward.harvestedAmount); } function getVaultRewardByTaprootAndIntegration( string calldata taprootPubKey, address integration, address rewardToken - ) external view returns (uint256 lastClaimedAt, uint256 pendingAmount) { + ) external view returns (uint256 lastClaimedAt, uint256 harvestedAmount) { UserReward storage reward = _vaults[ uuidByTaprootAndIntegration[ _createMappingKey(taprootPubKey, integration) ] ].rewards[rewardToken]; - return (reward.lastClaimedAt, reward.pendingAmount); + return (reward.lastClaimedAt, reward.harvestedAmount); } function getVaultAllocationDetails( @@ -567,6 +581,32 @@ contract PoolMerchant is // VAULT QUERIES // //////////////////////////////////////////////////////////////// + function _calculateVaultPendingRewards( + bytes32 uuid, + uint256[] memory integrationPendingRewards, + address[] memory _rewardTokens + ) internal view returns (uint256[] memory) { + VaultInfo storage vault = _vaults[uuid]; + Integration storage integ = integrations[vault.integration]; + + uint256[] memory vaultPendingRewards = new uint256[]( + _rewardTokens.length + ); + + // Only calculate if vault has shares + if (vault.shares > 0 && integ.totalShares > 0) { + for (uint256 i = 0; i < _rewardTokens.length; i++) { + if (integrationPendingRewards[i] > 0) { + vaultPendingRewards[i] = integrationPendingRewards[i] + .mul(vault.shares) + .div(integ.totalShares); + } + } + } + + return vaultPendingRewards; + } + // Get all vault UUIDs for a taproot public key function getVaultsByTaprootPubKey( string calldata taprootPubKey @@ -577,19 +617,7 @@ contract PoolMerchant is // Get details for a specific vault by UUID function getVaultDetails( bytes32 uuid - ) - public - view - returns ( - address integration, - uint256 shares, - uint256 valueMinted, - uint256 allocated, - uint256 unallocated, - uint256[] memory lastClaimedAt, - uint256[] memory pendingAmounts - ) - { + ) public view returns (VaultDetails memory details) { require(uuid != bytes32(0), "Invalid UUID"); VaultInfo storage vault = _vaults[uuid]; require(vault.integration != address(0), "Vault not found"); @@ -597,41 +625,47 @@ contract PoolMerchant is DLCLink.DLC memory dlc = dlcManager.getDLC(uuid); Integration storage integ = integrations[vault.integration]; - integration = vault.integration; - shares = vault.shares; - valueMinted = dlc.valueMinted; - allocated = vault.allocated; - unallocated = valueMinted - allocated; - // Get reward tokens from the integration strategy + // Get reward tokens and pending rewards from integration address[] memory _rewardTokens = integ.strategy.getRewardTokens(); + uint256[] memory integrationPendingRewards = integ + .strategy + .getPendingRewards(); + + // Calculate vault's share of unharvested rewards + uint256[] memory pendingRewards = _calculateVaultPendingRewards( + uuid, + integrationPendingRewards, + _rewardTokens + ); - // Get reward data - lastClaimedAt = new uint256[](_rewardTokens.length); - pendingAmounts = new uint256[](_rewardTokens.length); + // Get harvested rewards waiting to be claimed + uint256[] memory harvestedRewards = new uint256[](_rewardTokens.length); + uint256[] memory lastClaimedAt = new uint256[](_rewardTokens.length); for (uint256 i = 0; i < _rewardTokens.length; i++) { UserReward storage reward = vault.rewards[_rewardTokens[i]]; - lastClaimedAt[i] = reward.lastClaimedAt; - pendingAmounts[i] = reward.pendingAmount; + harvestedRewards[i] = reward.harvestedAmount; // Rewards harvested but not yet claimed + lastClaimedAt[i] = reward.lastClaimedAt; // Last time rewards were claimed } + + return + VaultDetails({ + integration: vault.integration, + shares: vault.shares, + valueMinted: dlc.valueMinted, + allocated: vault.allocated, + unallocated: dlc.valueMinted - vault.allocated, + rewardTokens: _rewardTokens, + lastClaimedAt: lastClaimedAt, + harvestedRewards: harvestedRewards, // Ready to be claimed + pendingRewards: pendingRewards // Still in integration + }); } function getVaultDetailsByTaprootAndIntegration( string calldata taprootPubKey, address _integration - ) - external - view - returns ( - address integration, - uint256 shares, - uint256 valueMinted, - uint256 allocated, - uint256 unallocated, - uint256[] memory lastClaimedAt, - uint256[] memory pendingAmounts - ) - { + ) external view returns (VaultDetails memory details) { bytes32 uuid = uuidByTaprootAndIntegration[ _createMappingKey(taprootPubKey, _integration) ]; From 362ac1c81bea7c9ac378f554c01f7eefb3260728 Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 7 Nov 2024 19:28:25 +0100 Subject: [PATCH 20/27] chore: better scripts --- contracts/PoolMerchant.sol | 4 +++- scripts/pool_merchant_setup_flow.js | 31 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index d7ecab1..0b171b6 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -74,6 +74,7 @@ contract PoolMerchant is } struct VaultDetails { + bytes32 uuid; address integration; uint256 shares; uint256 valueMinted; @@ -354,7 +355,7 @@ contract PoolMerchant is function allocateToIntegration( string calldata taprootPubKey, address integration - ) external onlyRole(ATTESTOR_ROLE) nonReentrant whenNotPaused { + ) public nonReentrant whenNotPaused { bytes32 uuid = uuidByTaprootAndIntegration[ _createMappingKey(taprootPubKey, integration) ]; @@ -650,6 +651,7 @@ contract PoolMerchant is return VaultDetails({ + uuid: uuid, integration: vault.integration, shares: vault.shares, valueMinted: dlc.valueMinted, diff --git a/scripts/pool_merchant_setup_flow.js b/scripts/pool_merchant_setup_flow.js index b8c71ed..3168aec 100644 --- a/scripts/pool_merchant_setup_flow.js +++ b/scripts/pool_merchant_setup_flow.js @@ -1,6 +1,10 @@ const hre = require('hardhat'); const { ethers, upgrades } = require('hardhat'); const { loadContractAddress } = require('./helpers/utils'); +const { + saveDeploymentInfo, + deploymentInfo, +} = require('./helpers/deployment-handlers_versioned'); async function main() { // Compile contracts @@ -23,11 +27,22 @@ async function main() { const newImplementation = await ethers.getContractFactory('DLCManager'); await upgrades.upgradeProxy(proxyAddress, newImplementation); + await hre.run('verify:verify', { + address: proxyAddress, + }); console.log('DLCManager upgraded'); console.log('\nšŸ“ Getting contract instances...'); const dlcManager = await ethers.getContractAt('DLCManager', proxyAddress); + try { + await saveDeploymentInfo( + deploymentInfo(network, dlcManager, 'DLCManager') + ); + } catch (error) { + console.error(error); + } + const dlcBTCAddress = await loadContractAddress('DLCBTC', network); const dlcBTC = await ethers.getContractAt('DLCBTC', dlcBTCAddress); @@ -41,6 +56,16 @@ async function main() { ]); await poolMerchant.deployed(); console.log('PoolMerchant deployed to:', poolMerchant.address); + try { + await saveDeploymentInfo( + deploymentInfo(network, poolMerchant, 'PoolMerchant') + ); + } catch (error) { + console.error(error); + } + await hre.run('verify:verify', { + address: poolMerchant.address, + }); // Whitelist PoolMerchant await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); @@ -51,6 +76,9 @@ async function main() { const mockVault = await MockERC4626Vault.deploy(dlcBTCAddress); await mockVault.deployed(); console.log('MockERC4626Vault deployed to:', mockVault.address); + await hre.run('verify:verify', { + address: mockVault.address, + }); console.log('\nšŸ—ļø Deploying IntegrationSample...'); const IntegrationSample = @@ -67,6 +95,9 @@ async function main() { ); await integrationSample.deployed(); console.log('IntegrationSample deployed to:', integrationSample.address); + await hre.run('verify:verify', { + address: integrationSample.address, + }); // Setup roles and integration console.log('\nšŸ”‘ Setting up roles and integration...'); From 77f03436d2c4044be07535110123464d8835bfd6 Mon Sep 17 00:00:00 2001 From: scolear Date: Thu, 7 Nov 2024 20:10:24 +0100 Subject: [PATCH 21/27] wip: arbsep deployment --- .openzeppelin/unknown-421614.json | 967 +++++++++++++++++++ deploymentFiles/arbsepolia/DLCManager.json | 17 +- deploymentFiles/arbsepolia/PoolMerchant.json | 87 ++ scripts/pool_merchant_setup_flow.js | 117 ++- 4 files changed, 1140 insertions(+), 48 deletions(-) create mode 100644 deploymentFiles/arbsepolia/PoolMerchant.json diff --git a/.openzeppelin/unknown-421614.json b/.openzeppelin/unknown-421614.json index 187c610..3b28776 100644 --- a/.openzeppelin/unknown-421614.json +++ b/.openzeppelin/unknown-421614.json @@ -84,6 +84,11 @@ "address": "0x6e692DB944162f8b4250aA25eCEe80608457D7a7", "txHash": "0xd6e5936315424a44930fa67eade8a23a84a4d2bad9edf5221b843a990a28a68c", "kind": "transparent" + }, + { + "address": "0xc0Fd850bE474e21449aF001782F11a0e9FC049C7", + "txHash": "0x84b0e6664753f61a3d608c050aa5e381427077f1562fbeb2e97f7fe04fb00428", + "kind": "transparent" } ], "impls": { @@ -11229,6 +11234,968 @@ }, "namespaces": {} } + }, + "1a486ddbfcb238bad9524bd895f8e98a0694a27b3a619359be1620033ebcfa1f": { + "address": "0x39a8Ac54Ba0Ff8ee4CB83B5d5FC32C6b9d7E425F", + "txHash": "0x7dd69e990c7080895702c8fb7b6948af862786527be6d91cd454990d5a50482c", + "layout": { + "solcVersion": "0.8.18", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)852_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_pendingDefaultAdmin", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:43" + }, + { + "label": "_pendingDefaultAdminSchedule", + "offset": 20, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:44" + }, + { + "label": "_currentDelay", + "offset": 26, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:46" + }, + { + "label": "_currentDefaultAdmin", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:47" + }, + { + "label": "_pendingDelay", + "offset": 20, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:50" + }, + { + "label": "_pendingDelaySchedule", + "offset": 26, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:394" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_index", + "offset": 0, + "slot": "251", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:49" + }, + { + "label": "dlcs", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_uint256,t_struct(DLC)8807_storage)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:50" + }, + { + "label": "dlcIDsByUUID", + "offset": 0, + "slot": "253", + "type": "t_mapping(t_bytes32,t_uint256)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:51" + }, + { + "label": "_minimumThreshold", + "offset": 0, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:53" + }, + { + "label": "_threshold", + "offset": 2, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:54" + }, + { + "label": "_signerCount", + "offset": 4, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:55" + }, + { + "label": "tssCommitment", + "offset": 0, + "slot": "255", + "type": "t_bytes32", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:56" + }, + { + "label": "attestorGroupPubKey", + "offset": 0, + "slot": "256", + "type": "t_string_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:57" + }, + { + "label": "dlcBTC", + "offset": 0, + "slot": "257", + "type": "t_contract(DLCBTC)8765", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:59" + }, + { + "label": "btcFeeRecipient", + "offset": 0, + "slot": "258", + "type": "t_string_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:60" + }, + { + "label": "minimumDeposit", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:61" + }, + { + "label": "maximumDeposit", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:62" + }, + { + "label": "btcMintFeeRate", + "offset": 0, + "slot": "261", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:63" + }, + { + "label": "btcRedeemFeeRate", + "offset": 0, + "slot": "262", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:64" + }, + { + "label": "whitelistingEnabled", + "offset": 0, + "slot": "263", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:65" + }, + { + "label": "userVaults", + "offset": 0, + "slot": "264", + "type": "t_mapping(t_address,t_array(t_bytes32)dyn_storage)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:67" + }, + { + "label": "_whitelistedAddresses", + "offset": 0, + "slot": "265", + "type": "t_mapping(t_address,t_bool)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:68" + }, + { + "label": "porEnabled", + "offset": 0, + "slot": "266", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:69" + }, + { + "label": "dlcBTCPoRFeed", + "offset": 1, + "slot": "266", + "type": "t_contract(AggregatorV3Interface)104", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:70" + }, + { + "label": "_seenSigners", + "offset": 0, + "slot": "267", + "type": "t_mapping(t_address,t_mapping(t_bytes32,t_bool))", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:71" + }, + { + "label": "skipSignatureVerification", + "offset": 0, + "slot": "268", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:74" + }, + { + "label": "__gap", + "offset": 0, + "slot": "269", + "type": "t_array(t_uint256)38_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:76" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)38_storage": { + "label": "uint256[38]", + "numberOfBytes": "1216" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(AggregatorV3Interface)104": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(DLCBTC)8765": { + "label": "contract DLCBTC", + "numberOfBytes": "20" + }, + "t_enum(DLCStatus)8777": { + "label": "enum DLCLink.DLCStatus", + "members": [ + "READY", + "FUNDED", + "CLOSING", + "CLOSED", + "AUX_STATE_1", + "AUX_STATE_2", + "AUX_STATE_3", + "AUX_STATE_4", + "AUX_STATE_5" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_array(t_bytes32)dyn_storage)": { + "label": "mapping(address => bytes32[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_bytes32,t_bool))": { + "label": "mapping(address => mapping(bytes32 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)852_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(DLC)8807_storage)": { + "label": "mapping(uint256 => struct DLCLink.DLC)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(DLC)8807_storage": { + "label": "struct DLCLink.DLC", + "members": [ + { + "label": "uuid", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "protocolContract", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "timestamp", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "valueLocked", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "4" + }, + { + "label": "status", + "type": "t_enum(DLCStatus)8777", + "offset": 20, + "slot": "4" + }, + { + "label": "fundingTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "5" + }, + { + "label": "closingTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "btcFeeRecipient", + "type": "t_string_storage", + "offset": 0, + "slot": "7" + }, + { + "label": "btcMintFeeBasisPoints", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "btcRedeemFeeBasisPoints", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "taprootPubKey", + "type": "t_string_storage", + "offset": 0, + "slot": "10" + }, + { + "label": "valueMinted", + "type": "t_uint256", + "offset": 0, + "slot": "11" + }, + { + "label": "wdTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "12" + } + ], + "numberOfBytes": "416" + }, + "t_struct(RoleData)852_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "4e1821fdf8bfbd89ea21f91fc350f2775462ea450594d20c3c642f065c283109": { + "address": "0x965567Ef8a9AD6B10858Af32db80e0E083c6550E", + "txHash": "0xe77cf2fc43d886383921d8ea2e4ef5a83a55ecc9ebfc637b3ba537982e8d61de", + "layout": { + "solcVersion": "0.8.18", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)747_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_pendingDefaultAdmin", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:43" + }, + { + "label": "_pendingDefaultAdminSchedule", + "offset": 20, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:44" + }, + { + "label": "_currentDelay", + "offset": 26, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:46" + }, + { + "label": "_currentDefaultAdmin", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:47" + }, + { + "label": "_pendingDelay", + "offset": 20, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:50" + }, + { + "label": "_pendingDelaySchedule", + "offset": 26, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:394" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "251", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "dlcManager", + "offset": 0, + "slot": "301", + "type": "t_contract(IDLCManager)5547", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:55" + }, + { + "label": "dlcBTC", + "offset": 0, + "slot": "302", + "type": "t_contract(IERC20)5006", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:56" + }, + { + "label": "_vaults", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_bytes32,t_struct(VaultInfo)5612_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:97" + }, + { + "label": "vaultsByTaprootPubKey", + "offset": 0, + "slot": "304", + "type": "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:98" + }, + { + "label": "integrations", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_address,t_struct(Integration)5651_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:99" + }, + { + "label": "rewardTokens", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_address,t_struct(RewardToken)5593_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:100" + }, + { + "label": "uuidByTaprootAndIntegration", + "offset": 0, + "slot": "307", + "type": "t_mapping(t_bytes32,t_bytes32)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:101" + }, + { + "label": "activeIntegrations", + "offset": 0, + "slot": "308", + "type": "t_array(t_address)dyn_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:102" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)50_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:104" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDLCManager)5547": { + "label": "contract IDLCManager", + "numberOfBytes": "20" + }, + "t_contract(IERC20)5006": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IIntegration)7486": { + "label": "contract IIntegration", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Integration)5651_storage)": { + "label": "mapping(address => struct PoolMerchant.Integration)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(RewardToken)5593_storage)": { + "label": "mapping(address => struct PoolMerchant.RewardToken)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(UserReward)5598_storage)": { + "label": "mapping(address => struct PoolMerchant.UserReward)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes32)": { + "label": "mapping(bytes32 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)747_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(VaultInfo)5612_storage)": { + "label": "mapping(bytes32 => struct PoolMerchant.VaultInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)": { + "label": "mapping(string => bytes32[])", + "numberOfBytes": "32" + }, + "t_string_memory_ptr": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Integration)5651_storage": { + "label": "struct PoolMerchant.Integration", + "members": [ + { + "label": "strategy", + "type": "t_contract(IIntegration)7486", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + }, + { + "label": "totalShares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "supportedRewardTokens", + "type": "t_array(t_address)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "vaults", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RewardToken)5593_storage": { + "label": "struct PoolMerchant.RewardToken", + "members": [ + { + "label": "tokenAddress", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)747_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UserReward)5598_storage": { + "label": "struct PoolMerchant.UserReward", + "members": [ + { + "label": "lastClaimedAt", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "harvestedAmount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(VaultInfo)5612_storage": { + "label": "struct PoolMerchant.VaultInfo", + "members": [ + { + "label": "integration", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "shares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allocated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "rewards", + "type": "t_mapping(t_address,t_struct(UserReward)5598_storage)", + "offset": 0, + "slot": "3" + }, + { + "label": "integrationIndex", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_string_storage": { + "label": "string" + } + }, + "namespaces": {} + } } } } diff --git a/deploymentFiles/arbsepolia/DLCManager.json b/deploymentFiles/arbsepolia/DLCManager.json index c125abb..794de64 100644 --- a/deploymentFiles/arbsepolia/DLCManager.json +++ b/deploymentFiles/arbsepolia/DLCManager.json @@ -1,7 +1,7 @@ { "network": "arbsepolia", - "updatedAt": "2024-09-26T17:49:57.073Z", - "gitSHA": "b4622fe", + "updatedAt": "2024-11-07T18:53:17.855Z", + "gitSHA": "362ac1c", "contract": { "name": "DLCManager", "address": "0xE8FA6399d1b60968f04888F23Ed13Ed499C1Fd51", @@ -10,6 +10,7 @@ "constructor()", "error ClosingFundedVault()", "error ContractNotWhitelisted()", + "error DLCAlreadyExists(bytes32 uuid)", "error DLCNotFound()", "error DLCNotFunded()", "error DLCNotPending()", @@ -27,6 +28,7 @@ "error NoSignerRenouncement()", "error NotCreatorContract()", "error NotDLCAdmin()", + "error NotEnoughReserves(uint256 reserves, uint256 amount)", "error NotEnoughSignatures()", "error NotOwner()", "error NotWhitelisted()", @@ -50,8 +52,10 @@ "event SetBtcFeeRecipient(string btcFeeRecipient)", "event SetBtcMintFeeRate(uint256 newBtcMintFeeRate)", "event SetBtcRedeemFeeRate(uint256 newBtcRedeemFeeRate)", + "event SetDlcBTCPoRFeed(address feed)", "event SetMaximumDeposit(uint256 newMaximumDeposit)", "event SetMinimumDeposit(uint256 newMinimumDeposit)", + "event SetPorEnabled(bool enabled)", "event SetStatusFunded(bytes32 uuid, string btcTxId, address creator, uint256 newValueLocked, uint256 amountToMint)", "event SetStatusPending(bytes32 uuid, string btcTxId, address creator, string taprootPubKey, uint256 newValueLocked)", "event SetThreshold(uint16 newThreshold)", @@ -77,8 +81,10 @@ "function defaultAdminDelay() view returns (uint48)", "function defaultAdminDelayIncreaseWait() view returns (uint48)", "function dlcBTC() view returns (address)", + "function dlcBTCPoRFeed() view returns (address)", "function dlcIDsByUUID(bytes32) view returns (uint256)", "function dlcs(uint256) view returns (bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)", + "function generateUUID(address sender, uint256 nonce) view returns (bytes32)", "function getAllDLCs(uint256 startIndex, uint256 endIndex) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)[])", "function getAllVaultUUIDsForAddress(address owner) view returns (bytes32[])", "function getAllVaultsForAddress(address owner) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)[])", @@ -88,6 +94,7 @@ "function getRoleAdmin(bytes32 role) view returns (bytes32)", "function getSignerCount() view returns (uint16)", "function getThreshold() view returns (uint16)", + "function getTotalValueMintedInVaults() view returns (uint256)", "function getVault(bytes32 uuid) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId))", "function grantRole(bytes32 role, address account)", "function hasRole(bytes32 role, address account) view returns (bool)", @@ -100,6 +107,7 @@ "function paused() view returns (bool)", "function pendingDefaultAdmin() view returns (address newAdmin, uint48 schedule)", "function pendingDefaultAdminDelay() view returns (uint48 newDelay, uint48 schedule)", + "function porEnabled() view returns (bool)", "function renounceRole(bytes32 role, address account)", "function revokeRole(bytes32 role, address account)", "function rollbackDefaultAdminDelay()", @@ -109,15 +117,20 @@ "function setBtcMintFeeRate(uint256 newBtcMintFeeRate)", "function setBtcRedeemFeeRate(uint256 newBtcRedeemFeeRate)", "function setBurnerOnTokenContract(address burner)", + "function setDlcBTCPoRFeed(address feed)", "function setMaximumDeposit(uint256 newMaximumDeposit)", "function setMinimumDeposit(uint256 newMinimumDeposit)", "function setMinterOnTokenContract(address minter)", + "function setPorEnabled(bool enabled)", + "function setSkipSignatureVerification(bool skip)", "function setStatusFunded(bytes32 uuid, string btcTxId, bytes[] signatures, uint256 newValueLocked)", "function setStatusPending(bytes32 uuid, string wdTxId, bytes[] signatures, string taprootPubKey, uint256 newValueLocked)", "function setTSSCommitment(bytes32 commitment)", "function setThreshold(uint16 newThreshold)", "function setWhitelistingEnabled(bool isWhitelistingEnabled)", + "function setupPendingVault(string _taprootPubKey, string _wdTxId) returns (bytes32)", "function setupVault() returns (bytes32)", + "function skipSignatureVerification() view returns (bool)", "function supportsInterface(bytes4 interfaceId) view returns (bool)", "function transferTokenContractOwnership(address newOwner)", "function tssCommitment() view returns (bytes32)", diff --git a/deploymentFiles/arbsepolia/PoolMerchant.json b/deploymentFiles/arbsepolia/PoolMerchant.json new file mode 100644 index 0000000..ebad9cd --- /dev/null +++ b/deploymentFiles/arbsepolia/PoolMerchant.json @@ -0,0 +1,87 @@ +{ + "network": "arbsepolia", + "updatedAt": "2024-11-07T18:53:25.584Z", + "gitSHA": "362ac1c", + "contract": { + "name": "PoolMerchant", + "address": "0xc0Fd850bE474e21449aF001782F11a0e9FC049C7", + "signerAddress": "0xBf7184178d610D7B0239a5CB8D64c1Df22d306a9", + "abi": [ + "constructor()", + "event DefaultAdminDelayChangeCanceled()", + "event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule)", + "event DefaultAdminTransferCanceled()", + "event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule)", + "event Initialized(uint8 version)", + "event IntegrationAdded(address indexed integration, address[] supportedRewards)", + "event Paused(address account)", + "event PendingVaultCreated(bytes32 indexed uuid, string taprootPubKey, string wdPSBT, address integration)", + "event RewardTokenAdded(address indexed token)", + "event RewardsClaimed(bytes32 indexed uuid, address indexed rewardToken, uint256 amount)", + "event RewardsHarvested(address indexed integration, address indexed rewardToken, address indexed harvester, uint256 amount)", + "event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)", + "event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)", + "event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)", + "event SharesAllocated(bytes32 indexed uuid, address indexed integration, uint256 shares)", + "event Unpaused(address account)", + "event VaultWithdrawn(bytes32 indexed uuid, uint256 amount)", + "function ATTESTOR_ROLE() view returns (bytes32)", + "function DEFAULT_ADMIN_ROLE() view returns (bytes32)", + "function HARVESTER_ROLE() view returns (bytes32)", + "function acceptDefaultAdminTransfer()", + "function activeIntegrations(uint256) view returns (address)", + "function addRewardToken(address token)", + "function allocateToIntegration(string taprootPubKey, address integration)", + "function beginDefaultAdminTransfer(address newAdmin)", + "function cancelDefaultAdminTransfer()", + "function changeDefaultAdminDelay(uint48 newDelay)", + "function claimRewards(string taprootPubKey, address integration, address rewardToken)", + "function createPendingVault(string taprootPubKey, string wdPSBT, address integration) returns (bytes32)", + "function defaultAdmin() view returns (address)", + "function defaultAdminDelay() view returns (uint48)", + "function defaultAdminDelayIncreaseWait() view returns (uint48)", + "function dlcBTC() view returns (address)", + "function dlcManager() view returns (address)", + "function getIntegrationVaults(address integration) view returns (bytes32[])", + "function getPendingIntegrationRewards(address integration) view returns (address[] tokens, uint256[] amounts)", + "function getRoleAdmin(bytes32 role) view returns (bytes32)", + "function getUnallocatedAmount(bytes32 uuid) view returns (uint256)", + "function getUnallocatedAmountByTaprootAndIntegration(string taprootPubKey, address integration) view returns (uint256)", + "function getVaultAllocationDetails(bytes32 uuid) view returns (uint256 valueMinted, uint256 allocated, uint256 unallocated)", + "function getVaultAllocationDetailsByTaprootAndIntegration(string taprootPubKey, address integration) view returns (uint256 valueMinted, uint256 allocated, uint256 unallocated)", + "function getVaultByTaprootAndIntegration(string taprootPubKey, address integration) view returns (bytes32)", + "function getVaultDetails(bytes32 uuid) view returns (tuple(bytes32 uuid, address integration, uint256 shares, uint256 valueMinted, uint256 allocated, uint256 unallocated, address[] rewardTokens, uint256[] lastClaimedAt, uint256[] harvestedRewards, uint256[] pendingRewards) details)", + "function getVaultDetailsByTaprootAndIntegration(string taprootPubKey, address _integration) view returns (tuple(bytes32 uuid, address integration, uint256 shares, uint256 valueMinted, uint256 allocated, uint256 unallocated, address[] rewardTokens, uint256[] lastClaimedAt, uint256[] harvestedRewards, uint256[] pendingRewards) details)", + "function getVaultIntegration(bytes32 uuid) view returns (address)", + "function getVaultIntegrationByTaprootAndIntegration(string taprootPubKey, address integration) view returns (address)", + "function getVaultReward(bytes32 uuid, address rewardToken) view returns (uint256 lastClaimedAt, uint256 harvestedAmount)", + "function getVaultRewardByTaprootAndIntegration(string taprootPubKey, address integration, address rewardToken) view returns (uint256 lastClaimedAt, uint256 harvestedAmount)", + "function getVaultShares(bytes32 uuid) view returns (uint256)", + "function getVaultSharesByTaprootAndIntegration(string taprootPubKey, address integration) view returns (uint256)", + "function getVaultsByTaprootPubKey(string taprootPubKey) view returns (bytes32[])", + "function grantRole(bytes32 role, address account)", + "function harvestRewardsForIntegration(address integration)", + "function hasRole(bytes32 role, address account) view returns (bool)", + "function initialize(address _dlcManager, address _dlcBTC, address defaultAdmin)", + "function integrations(address) view returns (address strategy, bool isActive, uint256 totalShares)", + "function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) returns (bytes4)", + "function onERC1155Received(address, address, uint256, uint256, bytes) returns (bytes4)", + "function onERC721Received(address, address, uint256, bytes) returns (bytes4)", + "function owner() view returns (address)", + "function pause()", + "function paused() view returns (bool)", + "function pendingDefaultAdmin() view returns (address newAdmin, uint48 schedule)", + "function pendingDefaultAdminDelay() view returns (uint48 newDelay, uint48 schedule)", + "function renounceRole(bytes32 role, address account)", + "function revokeRole(bytes32 role, address account)", + "function rewardTokens(address) view returns (address tokenAddress, bool isActive)", + "function rollbackDefaultAdminDelay()", + "function setIntegration(address integration, address[] supportedRewards)", + "function supportsInterface(bytes4 interfaceId) view returns (bool)", + "function unpause()", + "function uuidByTaprootAndIntegration(bytes32) view returns (bytes32)", + "function vaultsByTaprootPubKey(string, uint256) view returns (bytes32)", + "function withdrawFromVault(string taprootPubKey, address integration, uint256 amount)" + ] + } +} diff --git a/scripts/pool_merchant_setup_flow.js b/scripts/pool_merchant_setup_flow.js index 3168aec..1900930 100644 --- a/scripts/pool_merchant_setup_flow.js +++ b/scripts/pool_merchant_setup_flow.js @@ -1,12 +1,20 @@ +require('dotenv').config(); const hre = require('hardhat'); const { ethers, upgrades } = require('hardhat'); -const { loadContractAddress } = require('./helpers/utils'); +const { loadContractAddress, promptUser } = require('./helpers/utils'); const { saveDeploymentInfo, deploymentInfo, } = require('./helpers/deployment-handlers_versioned'); async function main() { + const network = hre.network.name; + const shouldContinue = await promptUser( + `You are about to interact with ${network}.\n Continue?` + ); + if (!shouldContinue) { + throw new Error('Deployment aborted by user.'); + } // Compile contracts console.log('\nšŸ”Ø Compiling contracts...'); await hre.run('compile'); @@ -16,69 +24,81 @@ async function main() { const accounts = await ethers.getSigners(); const deployer = accounts[0]; const dlcAdmin = deployer; - const attestor_1 = ''; // TODO: + const attestor_1 = '0x3355977947F84C2b1CAE7D2903a72958aEE185e2'; // TODO: devnet attestor 1 const operator = deployer; const harvester = deployer; // DLCManager upgrade remains the same console.log('\nšŸ”„ Upgrading DLCManager...'); - const network = hre.network.name; const proxyAddress = await loadContractAddress('DLCManager', network); - const newImplementation = await ethers.getContractFactory('DLCManager'); - await upgrades.upgradeProxy(proxyAddress, newImplementation); + // const newImplementation = await ethers.getContractFactory('DLCManager'); + // await upgrades.upgradeProxy(proxyAddress, newImplementation); - await hre.run('verify:verify', { - address: proxyAddress, - }); - console.log('DLCManager upgraded'); + // await hre.run('verify:verify', { + // address: proxyAddress, + // }); + // console.log('DLCManager upgraded'); console.log('\nšŸ“ Getting contract instances...'); const dlcManager = await ethers.getContractAt('DLCManager', proxyAddress); - try { - await saveDeploymentInfo( - deploymentInfo(network, dlcManager, 'DLCManager') - ); - } catch (error) { - console.error(error); - } + // try { + // await saveDeploymentInfo( + // deploymentInfo(network, dlcManager, 'DLCManager') + // ); + // } catch (error) { + // console.error(error); + // } const dlcBTCAddress = await loadContractAddress('DLCBTC', network); const dlcBTC = await ethers.getContractAt('DLCBTC', dlcBTCAddress); // Deploy PoolMerchant console.log('\nšŸ—ļø Deploying PoolMerchant...'); - const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); - const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ - proxyAddress, - dlcBTCAddress, - deployer.address, - ]); - await poolMerchant.deployed(); - console.log('PoolMerchant deployed to:', poolMerchant.address); - try { - await saveDeploymentInfo( - deploymentInfo(network, poolMerchant, 'PoolMerchant') - ); - } catch (error) { - console.error(error); - } - await hre.run('verify:verify', { - address: poolMerchant.address, - }); - - // Whitelist PoolMerchant - await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); + const poolMerchantAddress = await loadContractAddress( + 'PoolMerchant', + network + ); + const poolMerchant = await ethers.getContractAt( + 'PoolMerchant', + poolMerchantAddress + ); + // const PoolMerchant = await ethers.getContractFactory('PoolMerchant'); + // const poolMerchant = await upgrades.deployProxy(PoolMerchant, [ + // proxyAddress, + // dlcBTCAddress, + // deployer.address, + // ]); + // await poolMerchant.deployed(); + // console.log('PoolMerchant deployed to:', poolMerchant.address); + // try { + // await saveDeploymentInfo( + // deploymentInfo(network, poolMerchant, 'PoolMerchant') + // ); + // } catch (error) { + // console.error(error); + // } + // await hre.run('verify:verify', { + // address: poolMerchant.address, + // }); + + // // Whitelist PoolMerchant + // await dlcManager.connect(dlcAdmin).whitelistAddress(poolMerchant.address); console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); const MockERC4626Vault = await ethers.getContractFactory('MockERC4626Vault'); - const mockVault = await MockERC4626Vault.deploy(dlcBTCAddress); - await mockVault.deployed(); - console.log('MockERC4626Vault deployed to:', mockVault.address); - await hre.run('verify:verify', { - address: mockVault.address, - }); + + const mockVault = await ethers.getContractAt( + 'MockERC4626Vault', + '0x7B4c1663945767426177277Fa04777912720D39A' + ); + // const mockVault = await MockERC4626Vault.deploy(dlcBTCAddress); + // await mockVault.deployed(); + // console.log('MockERC4626Vault deployed to:', mockVault.address); + // await hre.run('verify:verify', { + // address: mockVault.address, + // }); console.log('\nšŸ—ļø Deploying IntegrationSample...'); const IntegrationSample = @@ -95,9 +115,14 @@ async function main() { ); await integrationSample.deployed(); console.log('IntegrationSample deployed to:', integrationSample.address); - await hre.run('verify:verify', { - address: integrationSample.address, - }); + + try { + await hre.run('verify:verify', { + address: integrationSample.address, + }); + } catch (error) { + console.error(error); + } // Setup roles and integration console.log('\nšŸ”‘ Setting up roles and integration...'); From f8bab99176e218630cac4640a992141d75598cc8 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 8 Nov 2024 09:38:15 +0100 Subject: [PATCH 22/27] feat: deployscript and getAllActiveINtegrations --- .openzeppelin/unknown-421614.json | 446 ++++++++++++++++++- contracts/PoolMerchant.sol | 8 + deploymentFiles/arbsepolia/PoolMerchant.json | 5 +- scripts/99_contract-configs.js | 45 ++ 4 files changed, 500 insertions(+), 4 deletions(-) diff --git a/.openzeppelin/unknown-421614.json b/.openzeppelin/unknown-421614.json index 3b28776..9b9144a 100644 --- a/.openzeppelin/unknown-421614.json +++ b/.openzeppelin/unknown-421614.json @@ -11758,6 +11758,448 @@ "4e1821fdf8bfbd89ea21f91fc350f2775462ea450594d20c3c642f065c283109": { "address": "0x965567Ef8a9AD6B10858Af32db80e0E083c6550E", "txHash": "0xe77cf2fc43d886383921d8ea2e4ef5a83a55ecc9ebfc637b3ba537982e8d61de", + "layout": { + "solcVersion": "0.8.18", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)852_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_pendingDefaultAdmin", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:43" + }, + { + "label": "_pendingDefaultAdminSchedule", + "offset": 20, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:44" + }, + { + "label": "_currentDelay", + "offset": 26, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:46" + }, + { + "label": "_currentDefaultAdmin", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:47" + }, + { + "label": "_pendingDelay", + "offset": 20, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:50" + }, + { + "label": "_pendingDelaySchedule", + "offset": 26, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:394" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "251", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "dlcManager", + "offset": 0, + "slot": "301", + "type": "t_contract(IDLCManager)12735", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:55" + }, + { + "label": "dlcBTC", + "offset": 0, + "slot": "302", + "type": "t_contract(IERC20)7802", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:56" + }, + { + "label": "_vaults", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_bytes32,t_struct(VaultInfo)12800_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:97" + }, + { + "label": "vaultsByTaprootPubKey", + "offset": 0, + "slot": "304", + "type": "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:98" + }, + { + "label": "integrations", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_address,t_struct(Integration)12839_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:99" + }, + { + "label": "rewardTokens", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_address,t_struct(RewardToken)12781_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:100" + }, + { + "label": "uuidByTaprootAndIntegration", + "offset": 0, + "slot": "307", + "type": "t_mapping(t_bytes32,t_bytes32)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:101" + }, + { + "label": "activeIntegrations", + "offset": 0, + "slot": "308", + "type": "t_array(t_address)dyn_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:102" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)50_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:104" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDLCManager)12735": { + "label": "contract IDLCManager", + "numberOfBytes": "20" + }, + "t_contract(IERC20)7802": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IIntegration)16156": { + "label": "contract IIntegration", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Integration)12839_storage)": { + "label": "mapping(address => struct PoolMerchant.Integration)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(RewardToken)12781_storage)": { + "label": "mapping(address => struct PoolMerchant.RewardToken)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(UserReward)12786_storage)": { + "label": "mapping(address => struct PoolMerchant.UserReward)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes32)": { + "label": "mapping(bytes32 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)852_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(VaultInfo)12800_storage)": { + "label": "mapping(bytes32 => struct PoolMerchant.VaultInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)": { + "label": "mapping(string => bytes32[])", + "numberOfBytes": "32" + }, + "t_string_memory_ptr": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Integration)12839_storage": { + "label": "struct PoolMerchant.Integration", + "members": [ + { + "label": "strategy", + "type": "t_contract(IIntegration)16156", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + }, + { + "label": "totalShares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "supportedRewardTokens", + "type": "t_array(t_address)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "vaults", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RewardToken)12781_storage": { + "label": "struct PoolMerchant.RewardToken", + "members": [ + { + "label": "tokenAddress", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)852_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UserReward)12786_storage": { + "label": "struct PoolMerchant.UserReward", + "members": [ + { + "label": "lastClaimedAt", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "harvestedAmount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(VaultInfo)12800_storage": { + "label": "struct PoolMerchant.VaultInfo", + "members": [ + { + "label": "integration", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "shares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allocated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "rewards", + "type": "t_mapping(t_address,t_struct(UserReward)12786_storage)", + "offset": 0, + "slot": "3" + }, + { + "label": "integrationIndex", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_string_storage": { + "label": "string" + } + }, + "namespaces": {} + } + }, + "1a49c8802e770b7ee2328b952cb3af4ae9a7d3cd0a3496e5155ab81dfb5ef2b7": { + "address": "0x9b6eE1041ED0616bACD077a919fAB97Ef55cd068", + "txHash": "0x7ad3b43e588c935fa63288904c3adde3be147cfedd69f2c321da041ea8262862", "layout": { "solcVersion": "0.8.18", "storage": [ @@ -12012,7 +12454,7 @@ "label": "contract IERC20", "numberOfBytes": "20" }, - "t_contract(IIntegration)7486": { + "t_contract(IIntegration)7495": { "label": "contract IIntegration", "numberOfBytes": "20" }, @@ -12057,7 +12499,7 @@ "members": [ { "label": "strategy", - "type": "t_contract(IIntegration)7486", + "type": "t_contract(IIntegration)7495", "offset": 0, "slot": "0" }, diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 0b171b6..4ff5420 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -456,6 +456,14 @@ contract PoolMerchant is ]; } + function getAllActiveIntegrations() + external + view + returns (address[] memory) + { + return activeIntegrations; + } + //////////////////////////////////////////////////////////////// // VAULT FUNCTIONS // //////////////////////////////////////////////////////////////// diff --git a/deploymentFiles/arbsepolia/PoolMerchant.json b/deploymentFiles/arbsepolia/PoolMerchant.json index ebad9cd..b5ce0c6 100644 --- a/deploymentFiles/arbsepolia/PoolMerchant.json +++ b/deploymentFiles/arbsepolia/PoolMerchant.json @@ -1,7 +1,7 @@ { "network": "arbsepolia", - "updatedAt": "2024-11-07T18:53:25.584Z", - "gitSHA": "362ac1c", + "updatedAt": "2024-11-08T08:36:51.728Z", + "gitSHA": "77f0343", "contract": { "name": "PoolMerchant", "address": "0xc0Fd850bE474e21449aF001782F11a0e9FC049C7", @@ -42,6 +42,7 @@ "function defaultAdminDelayIncreaseWait() view returns (uint48)", "function dlcBTC() view returns (address)", "function dlcManager() view returns (address)", + "function getAllActiveIntegrations() view returns (address[])", "function getIntegrationVaults(address integration) view returns (bytes32[])", "function getPendingIntegrationRewards(address integration) view returns (address[] tokens, uint256[] amounts)", "function getRoleAdmin(bytes32 role) view returns (bytes32)", diff --git a/scripts/99_contract-configs.js b/scripts/99_contract-configs.js index fe0e0ab..99c9337 100644 --- a/scripts/99_contract-configs.js +++ b/scripts/99_contract-configs.js @@ -162,5 +162,50 @@ module.exports = function getContractConfigs(networkConfig, _btcFeeRecipient) { }); }, }, + { + name: 'PoolMerchant', + deployer: deployer.address, + upgradeable: true, + requirements: ['DLCManager', 'DLCBTC'], + deploy: async (requirementAddresses) => { + const DLCManagerAddress = requirementAddresses['DLCManager']; + const DLCBTCAddress = requirementAddresses['DLCBTC']; + if (!DLCManagerAddress) + throw new Error('DLCManager deployment not found.'); + if (!DLCBTCAddress) + throw new Error('DLCBTC deployment not found.'); + await beforeDeployment( + 'PoolMerchant', + `dlcManager: ${DLCManagerAddress}, \ + dlcBTC: ${DLCBTCAddress}, \ + deployer: ${deployer.address}`, + networkName + ); + const PoolMerchant = + await hardhat.ethers.getContractFactory('PoolMerchant'); + const poolMerchant = await hardhat.upgrades.deployProxy( + PoolMerchant, + [DLCManagerAddress, DLCBTCAddress, deployer.address] + ); + await poolMerchant.deployed(); + + await afterDeployment( + 'PoolMerchant', + poolMerchant, + networkName + ); + + return poolMerchant.address; + }, + verify: async () => { + const address = await loadContractAddress( + 'PoolMerchant', + networkName + ); + await hardhat.run('verify:verify', { + address: address, + }); + }, + }, ]; }; From 0aa3958d05349a676f696dd7d34924019f4a1ba9 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 8 Nov 2024 10:34:32 +0100 Subject: [PATCH 23/27] feat: icyAddress in dlcVault --- .openzeppelin/unknown-421614.json | 968 +++++++++++++++++++ contracts/DLCLinkLibrary.sol | 1 + contracts/DLCManager.sol | 16 +- contracts/PoolMerchant.sol | 42 +- deploymentFiles/arbsepolia/DLCManager.json | 18 +- deploymentFiles/arbsepolia/PoolMerchant.json | 4 +- hardhat.config.js | 1 + 7 files changed, 1029 insertions(+), 21 deletions(-) diff --git a/.openzeppelin/unknown-421614.json b/.openzeppelin/unknown-421614.json index 9b9144a..2bfea81 100644 --- a/.openzeppelin/unknown-421614.json +++ b/.openzeppelin/unknown-421614.json @@ -12638,6 +12638,974 @@ }, "namespaces": {} } + }, + "043edcc90cd1034788103111af244217dd31b89372e473e9163e08681f047622": { + "address": "0x2b0600467e6E9406c5dbF4772430aBb8d2b2C327", + "txHash": "0x8db41d3750e92fb3b3b22633c8b0a8ea29a82072d6de8dbf682cc58c08dab614", + "layout": { + "solcVersion": "0.8.18", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)793_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_pendingDefaultAdmin", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:43" + }, + { + "label": "_pendingDefaultAdminSchedule", + "offset": 20, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:44" + }, + { + "label": "_currentDelay", + "offset": 26, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:46" + }, + { + "label": "_currentDefaultAdmin", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:47" + }, + { + "label": "_pendingDelay", + "offset": 20, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:50" + }, + { + "label": "_pendingDelaySchedule", + "offset": 26, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:394" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_index", + "offset": 0, + "slot": "251", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:49" + }, + { + "label": "dlcs", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_uint256,t_struct(DLC)8307_storage)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:50" + }, + { + "label": "dlcIDsByUUID", + "offset": 0, + "slot": "253", + "type": "t_mapping(t_bytes32,t_uint256)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:51" + }, + { + "label": "_minimumThreshold", + "offset": 0, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:53" + }, + { + "label": "_threshold", + "offset": 2, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:54" + }, + { + "label": "_signerCount", + "offset": 4, + "slot": "254", + "type": "t_uint16", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:55" + }, + { + "label": "tssCommitment", + "offset": 0, + "slot": "255", + "type": "t_bytes32", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:56" + }, + { + "label": "attestorGroupPubKey", + "offset": 0, + "slot": "256", + "type": "t_string_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:57" + }, + { + "label": "dlcBTC", + "offset": 0, + "slot": "257", + "type": "t_contract(DLCBTC)8263", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:59" + }, + { + "label": "btcFeeRecipient", + "offset": 0, + "slot": "258", + "type": "t_string_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:60" + }, + { + "label": "minimumDeposit", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:61" + }, + { + "label": "maximumDeposit", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:62" + }, + { + "label": "btcMintFeeRate", + "offset": 0, + "slot": "261", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:63" + }, + { + "label": "btcRedeemFeeRate", + "offset": 0, + "slot": "262", + "type": "t_uint256", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:64" + }, + { + "label": "whitelistingEnabled", + "offset": 0, + "slot": "263", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:65" + }, + { + "label": "userVaults", + "offset": 0, + "slot": "264", + "type": "t_mapping(t_address,t_array(t_bytes32)dyn_storage)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:67" + }, + { + "label": "_whitelistedAddresses", + "offset": 0, + "slot": "265", + "type": "t_mapping(t_address,t_bool)", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:68" + }, + { + "label": "porEnabled", + "offset": 0, + "slot": "266", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:69" + }, + { + "label": "dlcBTCPoRFeed", + "offset": 1, + "slot": "266", + "type": "t_contract(AggregatorV3Interface)45", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:70" + }, + { + "label": "_seenSigners", + "offset": 0, + "slot": "267", + "type": "t_mapping(t_address,t_mapping(t_bytes32,t_bool))", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:71" + }, + { + "label": "skipSignatureVerification", + "offset": 0, + "slot": "268", + "type": "t_bool", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:74" + }, + { + "label": "__gap", + "offset": 0, + "slot": "269", + "type": "t_array(t_uint256)38_storage", + "contract": "DLCManager", + "src": "contracts/DLCManager.sol:76" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)38_storage": { + "label": "uint256[38]", + "numberOfBytes": "1216" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(AggregatorV3Interface)45": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(DLCBTC)8263": { + "label": "contract DLCBTC", + "numberOfBytes": "20" + }, + "t_enum(DLCStatus)8275": { + "label": "enum DLCLink.DLCStatus", + "members": [ + "READY", + "FUNDED", + "CLOSING", + "CLOSED", + "AUX_STATE_1", + "AUX_STATE_2", + "AUX_STATE_3", + "AUX_STATE_4", + "AUX_STATE_5" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_array(t_bytes32)dyn_storage)": { + "label": "mapping(address => bytes32[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_bytes32,t_bool))": { + "label": "mapping(address => mapping(bytes32 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)793_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(DLC)8307_storage)": { + "label": "mapping(uint256 => struct DLCLink.DLC)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(DLC)8307_storage": { + "label": "struct DLCLink.DLC", + "members": [ + { + "label": "uuid", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "protocolContract", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "timestamp", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "valueLocked", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "4" + }, + { + "label": "status", + "type": "t_enum(DLCStatus)8275", + "offset": 20, + "slot": "4" + }, + { + "label": "fundingTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "5" + }, + { + "label": "closingTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "btcFeeRecipient", + "type": "t_string_storage", + "offset": 0, + "slot": "7" + }, + { + "label": "btcMintFeeBasisPoints", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "btcRedeemFeeBasisPoints", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "taprootPubKey", + "type": "t_string_storage", + "offset": 0, + "slot": "10" + }, + { + "label": "valueMinted", + "type": "t_uint256", + "offset": 0, + "slot": "11" + }, + { + "label": "wdTxId", + "type": "t_string_storage", + "offset": 0, + "slot": "12" + }, + { + "label": "icyIntegrationAddress", + "type": "t_address", + "offset": 0, + "slot": "13" + } + ], + "numberOfBytes": "448" + }, + "t_struct(RoleData)793_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "a617e58166f08b435f9f273bbd5fcf1f1b5a938732c347567e5ca64327c818e0": { + "address": "0xaC345969c553DD637C9A42c110A18A36c233cfe7", + "txHash": "0x4c105ab35022aa0cf5ddbfc964857bfe53292ae73db7c38f19eb1abfd7f6cfd7", + "layout": { + "solcVersion": "0.8.18", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)793_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_pendingDefaultAdmin", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:43" + }, + { + "label": "_pendingDefaultAdminSchedule", + "offset": 20, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:44" + }, + { + "label": "_currentDelay", + "offset": 26, + "slot": "151", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:46" + }, + { + "label": "_currentDefaultAdmin", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:47" + }, + { + "label": "_pendingDelay", + "offset": 20, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:50" + }, + { + "label": "_pendingDelaySchedule", + "offset": 26, + "slot": "152", + "type": "t_uint48", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "AccessControlDefaultAdminRulesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol:394" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "251", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "dlcManager", + "offset": 0, + "slot": "301", + "type": "t_contract(IDLCManager)10285", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:56" + }, + { + "label": "dlcBTC", + "offset": 0, + "slot": "302", + "type": "t_contract(IERC20)6875", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:57" + }, + { + "label": "_vaults", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_bytes32,t_struct(VaultInfo)10350_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:98" + }, + { + "label": "vaultsByTaprootPubKey", + "offset": 0, + "slot": "304", + "type": "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:99" + }, + { + "label": "integrations", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_address,t_struct(Integration)10389_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:100" + }, + { + "label": "rewardTokens", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_address,t_struct(RewardToken)10331_storage)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:101" + }, + { + "label": "uuidByTaprootAndIntegration", + "offset": 0, + "slot": "307", + "type": "t_mapping(t_bytes32,t_bytes32)", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:102" + }, + { + "label": "activeIntegrations", + "offset": 0, + "slot": "308", + "type": "t_array(t_address)dyn_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:103" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)50_storage", + "contract": "PoolMerchant", + "src": "contracts/PoolMerchant.sol:105" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDLCManager)10285": { + "label": "contract IDLCManager", + "numberOfBytes": "20" + }, + "t_contract(IERC20)6875": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IIntegration)12289": { + "label": "contract IIntegration", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Integration)10389_storage)": { + "label": "mapping(address => struct PoolMerchant.Integration)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(RewardToken)10331_storage)": { + "label": "mapping(address => struct PoolMerchant.RewardToken)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(UserReward)10336_storage)": { + "label": "mapping(address => struct PoolMerchant.UserReward)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes32)": { + "label": "mapping(bytes32 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)793_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(VaultInfo)10350_storage)": { + "label": "mapping(bytes32 => struct PoolMerchant.VaultInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_string_memory_ptr,t_array(t_bytes32)dyn_storage)": { + "label": "mapping(string => bytes32[])", + "numberOfBytes": "32" + }, + "t_string_memory_ptr": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Integration)10389_storage": { + "label": "struct PoolMerchant.Integration", + "members": [ + { + "label": "strategy", + "type": "t_contract(IIntegration)12289", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + }, + { + "label": "totalShares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "supportedRewardTokens", + "type": "t_array(t_address)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "vaults", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RewardToken)10331_storage": { + "label": "struct PoolMerchant.RewardToken", + "members": [ + { + "label": "tokenAddress", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "isActive", + "type": "t_bool", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)793_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UserReward)10336_storage": { + "label": "struct PoolMerchant.UserReward", + "members": [ + { + "label": "lastClaimedAt", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "harvestedAmount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(VaultInfo)10350_storage": { + "label": "struct PoolMerchant.VaultInfo", + "members": [ + { + "label": "integration", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "shares", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allocated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "rewards", + "type": "t_mapping(t_address,t_struct(UserReward)10336_storage)", + "offset": 0, + "slot": "3" + }, + { + "label": "integrationIndex", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_string_storage": { + "label": "string" + } + }, + "namespaces": {} + } } } } diff --git a/contracts/DLCLinkLibrary.sol b/contracts/DLCLinkLibrary.sol index 0d7c469..9139efc 100644 --- a/contracts/DLCLinkLibrary.sol +++ b/contracts/DLCLinkLibrary.sol @@ -35,5 +35,6 @@ library DLCLink { string taprootPubKey; uint256 valueMinted; string wdTxId; + address icyIntegrationAddress; } } diff --git a/contracts/DLCManager.sol b/contracts/DLCManager.sol index e483e41..85da4dd 100644 --- a/contracts/DLCManager.sol +++ b/contracts/DLCManager.sol @@ -341,7 +341,7 @@ contract DLCManager is { bytes32 _uuid = generateUUID(msg.sender, _index); - dlcs[_index] = DLCLink.DLC({ + DLCLink.DLC memory newDLC = DLCLink.DLC({ uuid: _uuid, protocolContract: msg.sender, // deprecated valueLocked: 0, @@ -355,9 +355,12 @@ contract DLCManager is btcFeeRecipient: btcFeeRecipient, btcMintFeeBasisPoints: btcMintFeeRate, btcRedeemFeeBasisPoints: btcRedeemFeeRate, - taprootPubKey: "" + taprootPubKey: "", + icyIntegrationAddress: address(0) }); + dlcs[_index] = newDLC; + emit CreateDLC(_uuid, msg.sender, block.timestamp); dlcIDsByUUID[_uuid] = _index; @@ -370,10 +373,11 @@ contract DLCManager is // TODO: auth function setupPendingVault( string calldata _taprootPubKey, - string calldata _wdTxId + string calldata _wdTxId, + address icyIntegrationAddress ) public onlyWhitelisted whenNotPaused returns (bytes32) { bytes32 _uuid = generateUUID(msg.sender, _index); - dlcs[_index] = DLCLink.DLC({ + DLCLink.DLC memory newDLC = DLCLink.DLC({ uuid: _uuid, protocolContract: msg.sender, // deprecated valueLocked: 0, @@ -387,8 +391,10 @@ contract DLCManager is btcFeeRecipient: btcFeeRecipient, btcMintFeeBasisPoints: btcMintFeeRate, btcRedeemFeeBasisPoints: btcRedeemFeeRate, - taprootPubKey: _taprootPubKey + taprootPubKey: _taprootPubKey, + icyIntegrationAddress: icyIntegrationAddress }); + dlcs[_index] = newDLC; emit CreateDLC(_uuid, msg.sender, block.timestamp); diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index 4ff5420..cf0e8c1 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -24,7 +24,8 @@ import "@openzeppelin/contracts/utils/math/SafeMath.sol"; interface IDLCManager { function setupPendingVault( string calldata _taprootPubKey, - string calldata _wdTxId + string calldata _wdTxId, + address _integration ) external returns (bytes32); function withdraw(bytes32 uuid, uint256 amount) external; @@ -174,6 +175,14 @@ contract PoolMerchant is whenNotPaused returns (bytes32) { + return _createPendingVault(taprootPubKey, wdPSBT, integration); + } + + function _createPendingVault( + string memory taprootPubKey, + string memory wdPSBT, + address integration + ) internal returns (bytes32) { require(integrations[integration].isActive, "Integration not active"); bytes32 mappingKey = _createMappingKey(taprootPubKey, integration); @@ -182,16 +191,39 @@ contract PoolMerchant is "Vault already exists for this taproot-integration pair" ); - bytes32 _uuid = dlcManager.setupPendingVault(taprootPubKey, wdPSBT); + // Create vault separately to manage stack + bytes32 _uuid = _setupVaultInDLCManager( + taprootPubKey, + wdPSBT, + integration + ); + + // Initialize vault data separately + _initializeVault(_uuid, integration, taprootPubKey, mappingKey); + + emit PendingVaultCreated(_uuid, taprootPubKey, wdPSBT, integration); + return _uuid; + } + function _setupVaultInDLCManager( + string memory taprootPubKey, + string memory wdPSBT, + address integration + ) internal returns (bytes32) { + return dlcManager.setupPendingVault(taprootPubKey, wdPSBT, integration); + } + + function _initializeVault( + bytes32 _uuid, + address integration, + string memory taprootPubKey, + bytes32 mappingKey + ) internal { _vaults[_uuid].integration = integration; _addVaultToIntegration(_uuid, integration); vaultsByTaprootPubKey[taprootPubKey].push(_uuid); uuidByTaprootAndIntegration[mappingKey] = _uuid; - - emit PendingVaultCreated(_uuid, taprootPubKey, wdPSBT, integration); - return _uuid; } function withdrawFromVault( diff --git a/deploymentFiles/arbsepolia/DLCManager.json b/deploymentFiles/arbsepolia/DLCManager.json index 794de64..22bdd7c 100644 --- a/deploymentFiles/arbsepolia/DLCManager.json +++ b/deploymentFiles/arbsepolia/DLCManager.json @@ -1,7 +1,7 @@ { "network": "arbsepolia", - "updatedAt": "2024-11-07T18:53:17.855Z", - "gitSHA": "362ac1c", + "updatedAt": "2024-11-08T09:31:14.697Z", + "gitSHA": "f8bab99", "contract": { "name": "DLCManager", "address": "0xE8FA6399d1b60968f04888F23Ed13Ed499C1Fd51", @@ -83,19 +83,19 @@ "function dlcBTC() view returns (address)", "function dlcBTCPoRFeed() view returns (address)", "function dlcIDsByUUID(bytes32) view returns (uint256)", - "function dlcs(uint256) view returns (bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)", + "function dlcs(uint256) view returns (bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress)", "function generateUUID(address sender, uint256 nonce) view returns (bytes32)", - "function getAllDLCs(uint256 startIndex, uint256 endIndex) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)[])", + "function getAllDLCs(uint256 startIndex, uint256 endIndex) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress)[])", "function getAllVaultUUIDsForAddress(address owner) view returns (bytes32[])", - "function getAllVaultsForAddress(address owner) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId)[])", - "function getDLC(bytes32 uuid) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId))", - "function getDLCByIndex(uint256 index) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId))", + "function getAllVaultsForAddress(address owner) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress)[])", + "function getDLC(bytes32 uuid) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress))", + "function getDLCByIndex(uint256 index) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress))", "function getMinimumThreshold() view returns (uint16)", "function getRoleAdmin(bytes32 role) view returns (bytes32)", "function getSignerCount() view returns (uint16)", "function getThreshold() view returns (uint16)", "function getTotalValueMintedInVaults() view returns (uint256)", - "function getVault(bytes32 uuid) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId))", + "function getVault(bytes32 uuid) view returns (tuple(bytes32 uuid, address protocolContract, uint256 timestamp, uint256 valueLocked, address creator, uint8 status, string fundingTxId, string closingTxId, string btcFeeRecipient, uint256 btcMintFeeBasisPoints, uint256 btcRedeemFeeBasisPoints, string taprootPubKey, uint256 valueMinted, string wdTxId, address icyIntegrationAddress))", "function grantRole(bytes32 role, address account)", "function hasRole(bytes32 role, address account) view returns (bool)", "function initialize(address defaultAdmin, address dlcAdminRole, uint16 threshold, address tokenContract, string btcFeeRecipientToSet)", @@ -128,7 +128,7 @@ "function setTSSCommitment(bytes32 commitment)", "function setThreshold(uint16 newThreshold)", "function setWhitelistingEnabled(bool isWhitelistingEnabled)", - "function setupPendingVault(string _taprootPubKey, string _wdTxId) returns (bytes32)", + "function setupPendingVault(string _taprootPubKey, string _wdTxId, address icyIntegrationAddress) returns (bytes32)", "function setupVault() returns (bytes32)", "function skipSignatureVerification() view returns (bool)", "function supportsInterface(bytes4 interfaceId) view returns (bool)", diff --git a/deploymentFiles/arbsepolia/PoolMerchant.json b/deploymentFiles/arbsepolia/PoolMerchant.json index b5ce0c6..00f172e 100644 --- a/deploymentFiles/arbsepolia/PoolMerchant.json +++ b/deploymentFiles/arbsepolia/PoolMerchant.json @@ -1,7 +1,7 @@ { "network": "arbsepolia", - "updatedAt": "2024-11-08T08:36:51.728Z", - "gitSHA": "77f0343", + "updatedAt": "2024-11-08T09:31:33.116Z", + "gitSHA": "f8bab99", "contract": { "name": "PoolMerchant", "address": "0xc0Fd850bE474e21449aF001782F11a0e9FC049C7", diff --git a/hardhat.config.js b/hardhat.config.js index c69737c..3829257 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -33,6 +33,7 @@ module.exports = { solidity: { version: '0.8.18', settings: { + viaIR: true, optimizer: { enabled: true, runs: 200, From 5cc033ee19d112c700f7754c7dc00caf73cf4442 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 8 Nov 2024 12:08:59 +0100 Subject: [PATCH 24/27] fix: docker --- docker/hardhat.config.docker.js | 1 + docker/scripts/deploy-all.js | 69 ++++++++++++++++++++++++++++++--- scripts/99_contract-configs.js | 13 +++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/docker/hardhat.config.docker.js b/docker/hardhat.config.docker.js index 7347168..bcf89fb 100644 --- a/docker/hardhat.config.docker.js +++ b/docker/hardhat.config.docker.js @@ -9,6 +9,7 @@ module.exports = { solidity: { version: '0.8.18', settings: { + viaIR: true, optimizer: { enabled: true, runs: 200, diff --git a/docker/scripts/deploy-all.js b/docker/scripts/deploy-all.js index 71a3004..9b79297 100644 --- a/docker/scripts/deploy-all.js +++ b/docker/scripts/deploy-all.js @@ -16,6 +16,11 @@ async function main() { medium: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', critical: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }; // Hardhat default deployer account + const defaultSigners = [ + '0x976EA74026E726554dB657fA54763abd0C3a0aa9', // account[6] + '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', // account[7] + '0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f', // account[8] + ]; const contractConfigs = getContractConfigs( { @@ -37,20 +42,74 @@ async function main() { await contractConfig.deploy(reqs); } + console.log('\nšŸ“ Getting contract instances...'); + const proxyAddress = await loadContractAddress('DLCManager', network); + const dlcManager = await hardhat.ethers.getContractAt( + 'DLCManager', + proxyAddress + ); + const dlcBTCAddress = await loadContractAddress('DLCBTC', network); + const dlcBTC = await hardhat.ethers.getContractAt('DLCBTC', dlcBTCAddress); + const poolMerchantAddress = await loadContractAddress( + 'PoolMerchant', + network + ); + const poolMerchant = await hardhat.ethers.getContractAt( + 'PoolMerchant', + poolMerchantAddress + ); + + console.log('\nšŸ—ļø Deploying MockERC4626Vault...'); + const MockERC4626Vault = + await hardhat.ethers.getContractFactory('MockERC4626Vault'); + const mockVault = await MockERC4626Vault.deploy(dlcBTCAddress); + await mockVault.deployed(); + console.log('MockERC4626Vault deployed to:', mockVault.address); + + console.log('\nšŸ—ļø Deploying IntegrationSample...'); + const IntegrationSample = + await hardhat.ethers.getContractFactory('IntegrationSample'); + const rewardRatePerSecond = hardhat.ethers.BigNumber.from('317'); + const integrationSample = await IntegrationSample.deploy( + mockVault.address, + dlcBTCAddress, + rewardRatePerSecond, + poolMerchant.address, + dlcBTCAddress + ); + await integrationSample.deployed(); + console.log('IntegrationSample deployed to:', integrationSample.address); + console.log('Deployment complete'); + console.log('\nšŸ”‘ Setting up roles and integration...'); + await poolMerchant.grantRole( + await poolMerchant.ATTESTOR_ROLE(), + defaultSigners[0] + ); + await poolMerchant.grantRole( + await poolMerchant.HARVESTER_ROLE(), + deployer.address + ); + + console.log('\nšŸŖ™ Adding dlcBTC as reward token...'); + await poolMerchant.addRewardToken(dlcBTCAddress); + + console.log('\nšŸ”„ Setting up Integration...'); + await poolMerchant.setIntegration(integrationSample.address, [ + dlcBTCAddress, + ]); + // Adding signers - const defaultSigners = [ - '0x976EA74026E726554dB657fA54763abd0C3a0aa9', // account[6] - '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', // account[7] - '0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f', // account[8] - ]; + for (const signer of defaultSigners) { await addSigner(signer); } // Set whitelisting await setWhitelisting('false'); + + console.log('\nāœ… Setup sequence complete'); } // make sure we catch all errors diff --git a/scripts/99_contract-configs.js b/scripts/99_contract-configs.js index 99c9337..4bb31d5 100644 --- a/scripts/99_contract-configs.js +++ b/scripts/99_contract-configs.js @@ -195,6 +195,19 @@ module.exports = function getContractConfigs(networkConfig, _btcFeeRecipient) { networkName ); + try { + console.log('Whitelisting PoolMerchant...'); + const dlcManager = await hardhat.ethers.getContractAt( + 'DLCManager', + DLCManagerAddress + ); + await dlcManager + .connect(deployer) + .whitelistAddress(poolMerchant.address); + } catch (error) { + console.error(error); + } + return poolMerchant.address; }, verify: async () => { From f1733d6c98c5741179f5254c3b8b1cdc0bb52b32 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 8 Nov 2024 17:24:39 +0100 Subject: [PATCH 25/27] fix: dockerignore --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 615da49..7260bd2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ cache/ artifacts/ .openzeppelin/ .husky/ -contracts/mocks/ contracts/upgrades/ scripts/log.txt From 72daec0709bb8c10c2fba08ba4c2c6311b489a38 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 8 Nov 2024 17:25:05 +0100 Subject: [PATCH 26/27] 3.3.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a8122e..e691601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dlc-link-eth", - "version": "3.2.2", + "version": "3.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dlc-link-eth", - "version": "3.2.2", + "version": "3.3.0", "license": "ISC", "bin": { "dlc-link-eth": "scripts/index.js" diff --git a/package.json b/package.json index ef86f5f..d929d87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dlc-link-eth", - "version": "3.2.2", + "version": "3.3.0", "main": "scripts/index.js", "directories": { "test": "test" From 4c2de9ed020a597e4afa51785a9cd45106013e41 Mon Sep 17 00:00:00 2001 From: scolear Date: Mon, 11 Nov 2024 17:37:20 +0100 Subject: [PATCH 27/27] fix: allocation check --- contracts/PoolMerchant.sol | 50 +++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/contracts/PoolMerchant.sol b/contracts/PoolMerchant.sol index cf0e8c1..788ae47 100644 --- a/contracts/PoolMerchant.sol +++ b/contracts/PoolMerchant.sol @@ -238,33 +238,43 @@ contract PoolMerchant is require(dlc.uuid != bytes32(0), "Vault does not exist"); VaultInfo storage vault = _vaults[uuid]; - require(amount <= vault.allocated, "Amount exceeds allocation"); - Integration storage integ = integrations[integration]; - // Harvest any pending rewards - if (vault.shares > 0) { - _harvestRewardsForVault(uuid); - } + require(amount <= dlc.valueMinted, "Amount exceeds minted value"); - // Withdraw from integration - uint256 received = integ.strategy.withdraw(amount); + uint256 received; - vault.shares = vault.shares.sub(received, "Insufficient shares"); - vault.allocated = vault.allocated.sub( - received, - "Insufficient allocation" - ); - integrations[integration].totalShares = integrations[integration] - .totalShares - .sub(received, "Insufficient total shares"); + if (vault.allocated > 0) { + // If there are allocated funds, handle integration withdrawal + require(amount <= vault.allocated, "Amount exceeds allocation"); - // Clean up if fully withdrawn - if (vault.shares == 0) { - _removeVaultFromIntegration(uuid, integration); + // Harvest any pending rewards + if (vault.shares > 0) { + _harvestRewardsForVault(uuid); + } + + // Withdraw from integration + received = integ.strategy.withdraw(amount); + + vault.shares = vault.shares.sub(received, "Insufficient shares"); + vault.allocated = vault.allocated.sub( + received, + "Insufficient allocation" + ); + integrations[integration].totalShares = integrations[integration] + .totalShares + .sub(received, "Insufficient total shares"); + + // Clean up if fully withdrawn + if (vault.shares == 0) { + _removeVaultFromIntegration(uuid, integration); + } + } else { + // If no funds are allocated, simply withdraw the requested amount + received = amount; } - // Withdraw from DLCManager with adjusted received + // Withdraw from DLCManager with adjusted received amount dlcManager.withdraw(uuid, received); emit VaultWithdrawn(uuid, received); }