From 2a9c07c99c5d41242f6b9fdec1e28dda2b245cc7 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Fri, 10 Jan 2025 18:08:39 +0100 Subject: [PATCH 01/13] feat: add fragment contract --- .../partners/tokenWrappers/SonicFragment.sol | 71 ++ hardhat.config.ts | 696 ++++++++++++++++++ lib/forge-std | 1 + 3 files changed, 768 insertions(+) create mode 100644 contracts/partners/tokenWrappers/SonicFragment.sol create mode 100644 hardhat.config.ts create mode 160000 lib/forge-std diff --git a/contracts/partners/tokenWrappers/SonicFragment.sol b/contracts/partners/tokenWrappers/SonicFragment.sol new file mode 100644 index 0000000..7e7ebbd --- /dev/null +++ b/contracts/partners/tokenWrappers/SonicFragment.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.17; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol"; +import { Errors } from "../../utils/Errors.sol"; + +/// @title SonicFragment +/// @author Angle Labs, Inc. +contract SonicFragment is ERC2O { + using SafeERC20 for IERC20; + + /// @notice `AccessControlManager` contract handling access control + IAccessControlManager public accessControlManager; + uint256 public exchangeRate; + address public sToken; + uint8 public contractSettled; + + constructor( + address _accessControlManager, + address recipient, + uint256 _totalSupply, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + // Zero address check + IAccessControlManager(_accessControlManager).isGovernor(msg.sender); + accessControlManager = IAccessControlManager(_accessControlManager); + _mint(recipient, _totalSupply); + } + + // ================================= MODIFIERS ================================= + + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier isGovernor() { + if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotAllowed(); + _; + } + + /// @notice Activates the contract settlement and enables redemption of fragments into S + function settleContract(uint256 sTokenAmount) external isGovernor { + if (contractSettled > 0) revert Errors.NotAllowed(); + IERC20(sToken).safeTransferFrom(msg.sender, address(this), sTokenAmount); + contractSettled = 1; + uint256 _totalSupply = totalSupply(); + exchangeRate = (sTokenAmount * 1 ether) / _totalSupply; + } + + /// @notice Sets the S address + /// @dev Cannot be set once redemption is activated + function setSTokenAddress(address sTokenAddress) external isGovernor { + if (contractSettled == 0) sToken = sTokenAddress; + } + + /// @notice Redeems fragments against S + function redeem(uint256 amount, address recipient) external { + _burn(msg.sender, amount); + uint256 amountToSend = (amount * exchangeRate) / 1 ether; + IERC20(sToken).safeTransfer(recipient, amount); + } + + /// @notice Recovers leftover tokens after sometime + function recover(uint256 amount, address recipient) external onlyGovernor { + IERC20(sToken).safeTransfer(recipient, amount); + exchangeRate = 0; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..7b62326 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,696 @@ +/// ENVVAR +// - ENABLE_GAS_REPORT +// - CI +// - RUNS +import 'dotenv/config'; +import 'hardhat-contract-sizer'; +import 'hardhat-spdx-license-identifier'; +import 'hardhat-deploy'; +import 'hardhat-abi-exporter'; +import '@nomicfoundation/hardhat-chai-matchers'; /** NEW FEATURE - https://hardhat.org/hardhat-chai-matchers/docs/reference#.revertedwithcustomerror */ +import '@nomicfoundation/hardhat-toolbox'; /** NEW FEATURE */ +import '@openzeppelin/hardhat-upgrades'; +import '@nomiclabs/hardhat-truffle5'; +import '@nomiclabs/hardhat-solhint'; +import '@tenderly/hardhat-tenderly'; +import '@typechain/hardhat'; + +import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from 'hardhat/builtin-tasks/task-names'; +import { HardhatUserConfig, subtask } from 'hardhat/config'; +import { HardhatNetworkAccountsUserConfig } from 'hardhat/types'; +import yargs from 'yargs'; + +import { accounts, etherscanKey, getMerklAccount, getMnemonic, getPkey, nodeUrl } from './utils/network'; + +// Otherwise, ".sol" files from "test" are picked up during compilation and throw an error +subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction(async (_, __, runSuper) => { + const paths = await runSuper(); + return paths.filter((p: string) => !p.includes('/test/foundry/')); +}); + +const accountsPkey = [getPkey()]; +const accountsMerklDeployer: HardhatNetworkAccountsUserConfig = getMerklAccount(); + + +const config: HardhatUserConfig = { + solidity: { + compilers: [ + { + version: '0.8.24', + settings: { + optimizer: { + enabled: true, + runs: 100000, + }, + viaIR: false, + }, + }, + { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 100000, + }, + viaIR: false, + }, + }, + ], + overrides: { + 'contracts/DistributionCreator.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 1, + }, + viaIR: false, + }, + }, + 'contracts/mock/DistributionCreatorUpdatable.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 1, + }, + viaIR: false, + }, + }, + 'contracts/deprecated/OldDistributionCreator.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 1, + }, + viaIR: false, + }, + }, + }, + }, + defaultNetwork: 'hardhat', + // For the lists of Chain ID: https://chainlist.org + networks: { + hardhat: { + live: false, + blockGasLimit: 125e5, + initialBaseFeePerGas: 0, + hardfork: 'london', + accounts: accounts('worldchain'), + forking: { + enabled: false, + // Mainnet + /* + url: nodeUrl('fork'), + blockNumber: 19127150, + */ + // Polygon + /* + url: nodeUrl('forkpolygon'), + blockNumber: 39517477, + */ + // Optimism + /* + url: nodeUrl('optimism'), + blockNumber: 17614765, + */ + // Arbitrum + /* + url: nodeUrl('arbitrum'), + blockNumber: 19356874, + */ + /* + url: nodeUrl('arbitrum'), + blockNumber: 19356874, + */ + /* + url: nodeUrl('polygonzkevm'), + blockNumber: 3214816, + */ + /* + url: nodeUrl('coredao'), + */ + /* + url: nodeUrl('gnosis'), + blockNumber: 14188687, + */ + /* + url: nodeUrl('immutable'), + blockNumber: 3160413, + */ + /* + url: nodeUrl('manta'), + blockNumber: 1479731, + */ + /* + url: nodeUrl('scroll'), + blockNumber: 3670869, + */ + // url: nodeUrl('blast'), + // blockNumber: 421659, + // url: nodeUrl('fraxtal'), + // blockNumber: 6644000, + url: nodeUrl('worldchain'), + blockNumber: 4521455, + }, + mining: + { + auto: false, + interval: 1000, + }, + chainId: 42220, + }, + polygon: { + live: true, + url: nodeUrl('polygon'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 3, + chainId: 137, + gasPrice: 'auto', + verify: { + etherscan: { + apiKey: etherscanKey('polygon'), + }, + }, + }, + fantom: { + live: true, + url: nodeUrl('fantom'), + accounts: [getPkey()], + gas: 'auto', + chainId: 250, + verify: { + etherscan: { + apiKey: etherscanKey('fantom'), + }, + }, + }, + mainnet: { + live: true, + url: nodeUrl('mainnet'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 1, + verify: { + etherscan: { + apiKey: etherscanKey('mainnet'), + }, + }, + }, + optimism: { + live: true, + url: nodeUrl('optimism'), + accounts: [getPkey()], + gas: 'auto', + chainId: 10, + verify: { + etherscan: { + apiKey: etherscanKey('optimism'), + }, + }, + }, + arbitrum: { + live: true, + url: nodeUrl('arbitrum'), + accounts: [getPkey()], + gas: 'auto', + chainId: 42161, + verify: { + etherscan: { + apiKey: etherscanKey('arbitrum'), + }, + }, + }, + avalanche: { + live: true, + url: nodeUrl('avalanche'), + accounts: [getPkey()], + gas: 'auto', + chainId: 43114, + verify: { + etherscan: { + apiKey: etherscanKey('avalanche'), + }, + }, + }, + aurora: { + live: true, + url: nodeUrl('aurora'), + accounts: [getPkey()], + gas: 'auto', + chainId: 1313161554, + verify: { + etherscan: { + apiKey: etherscanKey('aurora'), + }, + }, + }, + bsc: { + live: true, + url: nodeUrl('bsc'), + accounts: [getPkey()], + gas: 60000000, + gasMultiplier: 3, + gasPrice: 'auto', + chainId: 56, + verify: { + etherscan: { + apiKey: etherscanKey('bsc'), + }, + }, + }, + gnosis: { + live: true, + url: nodeUrl('gnosis'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 3, + chainId: 100, + initialBaseFeePerGas: 2000000000, + verify: { + etherscan: { + apiKey: etherscanKey('gnosis'), + }, + }, + }, + polygonzkevm: { + live: true, + url: nodeUrl('polygonzkevm'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 1101, + verify: { + etherscan: { + apiKey: etherscanKey('polygonzkevm'), + }, + }, + }, + base: { + live: true, + url: nodeUrl('base'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 8453, + verify: { + etherscan: { + apiKey: etherscanKey('base'), + }, + }, + }, + bob: { + live: true, + url: nodeUrl('bob'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 60808, + verify: { + etherscan: { + apiKey: etherscanKey('bob'), + }, + }, + }, + linea: { + live: true, + url: nodeUrl('linea'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 59144, + verify: { + etherscan: { + apiKey: etherscanKey('linea'), + }, + }, + }, + zksync: { + live: true, + url: nodeUrl('zksync'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 324, + ethNetwork: nodeUrl('mainnet'), + verifyURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification', + verify: { + etherscan: { + apiKey: etherscanKey('zksync'), + }, + }, + zksync: true, + }, + mantle: { + live: true, + url: nodeUrl('mantle'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 5000, + verify: { + etherscan: { + apiKey: etherscanKey('mantle'), + }, + }, + }, + filecoin: { + live: true, + url: nodeUrl('filecoin'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 314, + verify: { + etherscan: { + apiKey: etherscanKey('filecoin'), + }, + }, + }, + blast: { + live: true, + url: nodeUrl('blast'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 81457, + verify: { + etherscan: { + apiKey: etherscanKey('blast'), + }, + }, + }, + mode: { + live: true, + url: nodeUrl('mode'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 34443, + verify: { + etherscan: { + apiKey: etherscanKey('mode'), + }, + }, + }, + thundercore: { + live: true, + url: nodeUrl('thundercore'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 108, + verify: { + etherscan: { + apiKey: etherscanKey('thundercore'), + }, + }, + }, + coredao: { + live: true, + url: nodeUrl('coredao'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 1116, + verify: { + etherscan: { + apiKey: etherscanKey('coredao'), + }, + }, + }, + xlayer: { + live: true, + url: nodeUrl('xlayer'), + accounts: [getPkey()], + gas: 'auto', + chainId: 196, + }, + taiko: { + live: true, + url: nodeUrl('taiko'), + accounts: [getPkey()], + gas: 'auto', + chainId: 167000, + }, + fuse: { + live: true, + url: nodeUrl('fuse'), + accounts: [getPkey()], + gas: 'auto', + chainId: 122, + }, + immutablezkevm: { + live: true, + url: nodeUrl('immutablezkevm'), + accounts: [getPkey()], + gas: 'auto', + gasPrice: 'auto', + gasMultiplier: 1.3, + chainId: 13371, + verify: { + etherscan: { + apiKey: etherscanKey('immutablezkevm'), + }, + }, + }, + immutable: { + live: true, + url: nodeUrl('immutable'), + accounts: [getPkey()], + gas: 'auto', + chainId: 13371, + verify: { + etherscan: { + apiKey: etherscanKey('immutable'), + }, + }, + }, + scroll: { + live: true, + url: nodeUrl('scroll'), + accounts: [getPkey()], + gas: 'auto', + chainId: 534352, + verify: { + etherscan: { + apiKey: etherscanKey('scroll'), + }, + }, + }, + manta: { + live: true, + url: nodeUrl('manta'), + accounts: [getPkey()], + gas: 'auto', + chainId: 169, + verify: { + etherscan: { + apiKey: etherscanKey('manta'), + }, + }, + }, + sei: { + live: true, + url: nodeUrl('sei'), + accounts: [getPkey()], + gas: 'auto', + chainId: 1329, + verify: { + etherscan: { + apiKey: etherscanKey('sei'), + }, + }, + }, + celo: { + live: true, + url: nodeUrl('celo'), + accounts: [getPkey()], + gas: 'auto', + chainId: 42220, + verify: { + etherscan: { + apiKey: etherscanKey('celo'), + }, + }, + }, + fraxtal: { + live: true, + url: nodeUrl('fraxtal'), + accounts: accountsMerklDeployer, + gas: 'auto', + chainId: 252, + verify: { + etherscan: { + apiKey: etherscanKey('fraxtal'), + }, + }, + }, + astar: { + live: true, + url: nodeUrl('astar'), + accounts: [getPkey()], + gas: 'auto', + gasPrice: 'auto', + chainId: 592, + verify: { + etherscan: { + apiKey: etherscanKey('astar'), + }, + }, + }, + astarzkevm: { + live: true, + url: nodeUrl('astarzkevm'), + accounts: [getPkey()], + gas: 'auto', + gasPrice: 'auto', + chainId: 3776, + verify: { + etherscan: { + apiKey: etherscanKey('astarzkevm'), + }, + }, + }, + rootstock: { + live: true, + url: nodeUrl('rootstock'), + accounts: accounts('rootstock'), + gas: 'auto', + chainId: 30, + verify: { + etherscan: { + apiKey: etherscanKey('rootstock'), + }, + }, + }, + moonbeam: { + live: true, + url: nodeUrl('moonbeam'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 1284, + verify: { + etherscan: { + apiKey: etherscanKey('moonbeam'), + }, + }, + }, + skale: { + live: true, + url: nodeUrl('skale'), + accounts: [getPkey()], + gas: 'auto', + gasMultiplier: 1.3, + chainId: 2046399126, + verify: { + etherscan: { + apiKey: etherscanKey('skale'), + }, + }, + }, + worldchain: { + live: true, + url: nodeUrl('worldchain'), + accounts: accountsMerklDeployer, + gas: 'auto', + chainId: 480, + verify: { + etherscan: { + apiKey: etherscanKey('worldchain'), + }, + }, + }, + }, + paths: { + sources: './contracts', + tests: './test', + cache: 'cache-hh', + }, + namedAccounts: { + deployer: 0, + guardian: 1, + governor: 2, + proxyAdmin: 3, + alice: 4, + bob: 5, + charlie: 6, + }, + contractSizer: { + alphaSort: true, + runOnCompile: false, + disambiguatePaths: false, + }, + gasReporter: { + currency: 'USD', + outputFile: 'gas-report.txt', + }, + spdxLicenseIdentifier: { + overwrite: false, + runOnCompile: true, + }, + abiExporter: { + path: './export/abi', + clear: true, + flat: true, + spacing: 2, + }, + etherscan: { + // apiKey: process.env.ETHERSCAN_API_KEY, + apiKey: { + worldchain: etherscanKey('worldchain'), + }, + customChains: [ + { + network: 'taiko', + chainId: 167000, + urls: { + apiURL: 'https://api.taikoscan.io/api', + browserURL: 'https://taikoscan.io/', + }, + }, + { + network: 'celo', + chainId: 42220, + urls: { + apiURL: 'https://api.celoscan.io/api', + browserURL: 'https://celoscan.io/', + }, + }, + { + network: 'fraxtal', + chainId: 252, + urls: { + apiURL: 'https://api.fraxscan.com/api', + browserURL: 'https://fraxscan.com/', + }, + }, + { + network: 'skale', + chainId: 2046399126, + urls: { + apiURL: 'https://internal-hubs.explorer.mainnet.skalenodes.com:10001/api', + browserURL: 'https://elated-tan-skat.explorer.mainnet.skalenodes.com/', + }, + }, + { + network: 'worldchain', + chainId: 480, + urls: { + apiURL: 'https://worldchain-mainnet.explorer.alchemy.com/api', + browserURL: 'https://worldchain-mainnet.explorer.alchemy.com/', + }, + }, + ], + }, + typechain: { + outDir: 'typechain', + target: 'ethers-v5', + }, +}; + +export default config; diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..cb69e9c --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit cb69e9c07fbd002819c8c6c8db3caeab76b90d6b From 62d66738ae69674d14fc9f8a18771dfd537a9a80 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Fri, 10 Jan 2025 18:09:08 +0100 Subject: [PATCH 02/13] rm: hardhat --- hardhat.config.ts | 696 ---------------------------------------------- 1 file changed, 696 deletions(-) delete mode 100644 hardhat.config.ts diff --git a/hardhat.config.ts b/hardhat.config.ts deleted file mode 100644 index 7b62326..0000000 --- a/hardhat.config.ts +++ /dev/null @@ -1,696 +0,0 @@ -/// ENVVAR -// - ENABLE_GAS_REPORT -// - CI -// - RUNS -import 'dotenv/config'; -import 'hardhat-contract-sizer'; -import 'hardhat-spdx-license-identifier'; -import 'hardhat-deploy'; -import 'hardhat-abi-exporter'; -import '@nomicfoundation/hardhat-chai-matchers'; /** NEW FEATURE - https://hardhat.org/hardhat-chai-matchers/docs/reference#.revertedwithcustomerror */ -import '@nomicfoundation/hardhat-toolbox'; /** NEW FEATURE */ -import '@openzeppelin/hardhat-upgrades'; -import '@nomiclabs/hardhat-truffle5'; -import '@nomiclabs/hardhat-solhint'; -import '@tenderly/hardhat-tenderly'; -import '@typechain/hardhat'; - -import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from 'hardhat/builtin-tasks/task-names'; -import { HardhatUserConfig, subtask } from 'hardhat/config'; -import { HardhatNetworkAccountsUserConfig } from 'hardhat/types'; -import yargs from 'yargs'; - -import { accounts, etherscanKey, getMerklAccount, getMnemonic, getPkey, nodeUrl } from './utils/network'; - -// Otherwise, ".sol" files from "test" are picked up during compilation and throw an error -subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction(async (_, __, runSuper) => { - const paths = await runSuper(); - return paths.filter((p: string) => !p.includes('/test/foundry/')); -}); - -const accountsPkey = [getPkey()]; -const accountsMerklDeployer: HardhatNetworkAccountsUserConfig = getMerklAccount(); - - -const config: HardhatUserConfig = { - solidity: { - compilers: [ - { - version: '0.8.24', - settings: { - optimizer: { - enabled: true, - runs: 100000, - }, - viaIR: false, - }, - }, - { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 100000, - }, - viaIR: false, - }, - }, - ], - overrides: { - 'contracts/DistributionCreator.sol': { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 1, - }, - viaIR: false, - }, - }, - 'contracts/mock/DistributionCreatorUpdatable.sol': { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 1, - }, - viaIR: false, - }, - }, - 'contracts/deprecated/OldDistributionCreator.sol': { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 1, - }, - viaIR: false, - }, - }, - }, - }, - defaultNetwork: 'hardhat', - // For the lists of Chain ID: https://chainlist.org - networks: { - hardhat: { - live: false, - blockGasLimit: 125e5, - initialBaseFeePerGas: 0, - hardfork: 'london', - accounts: accounts('worldchain'), - forking: { - enabled: false, - // Mainnet - /* - url: nodeUrl('fork'), - blockNumber: 19127150, - */ - // Polygon - /* - url: nodeUrl('forkpolygon'), - blockNumber: 39517477, - */ - // Optimism - /* - url: nodeUrl('optimism'), - blockNumber: 17614765, - */ - // Arbitrum - /* - url: nodeUrl('arbitrum'), - blockNumber: 19356874, - */ - /* - url: nodeUrl('arbitrum'), - blockNumber: 19356874, - */ - /* - url: nodeUrl('polygonzkevm'), - blockNumber: 3214816, - */ - /* - url: nodeUrl('coredao'), - */ - /* - url: nodeUrl('gnosis'), - blockNumber: 14188687, - */ - /* - url: nodeUrl('immutable'), - blockNumber: 3160413, - */ - /* - url: nodeUrl('manta'), - blockNumber: 1479731, - */ - /* - url: nodeUrl('scroll'), - blockNumber: 3670869, - */ - // url: nodeUrl('blast'), - // blockNumber: 421659, - // url: nodeUrl('fraxtal'), - // blockNumber: 6644000, - url: nodeUrl('worldchain'), - blockNumber: 4521455, - }, - mining: - { - auto: false, - interval: 1000, - }, - chainId: 42220, - }, - polygon: { - live: true, - url: nodeUrl('polygon'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 3, - chainId: 137, - gasPrice: 'auto', - verify: { - etherscan: { - apiKey: etherscanKey('polygon'), - }, - }, - }, - fantom: { - live: true, - url: nodeUrl('fantom'), - accounts: [getPkey()], - gas: 'auto', - chainId: 250, - verify: { - etherscan: { - apiKey: etherscanKey('fantom'), - }, - }, - }, - mainnet: { - live: true, - url: nodeUrl('mainnet'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 1, - verify: { - etherscan: { - apiKey: etherscanKey('mainnet'), - }, - }, - }, - optimism: { - live: true, - url: nodeUrl('optimism'), - accounts: [getPkey()], - gas: 'auto', - chainId: 10, - verify: { - etherscan: { - apiKey: etherscanKey('optimism'), - }, - }, - }, - arbitrum: { - live: true, - url: nodeUrl('arbitrum'), - accounts: [getPkey()], - gas: 'auto', - chainId: 42161, - verify: { - etherscan: { - apiKey: etherscanKey('arbitrum'), - }, - }, - }, - avalanche: { - live: true, - url: nodeUrl('avalanche'), - accounts: [getPkey()], - gas: 'auto', - chainId: 43114, - verify: { - etherscan: { - apiKey: etherscanKey('avalanche'), - }, - }, - }, - aurora: { - live: true, - url: nodeUrl('aurora'), - accounts: [getPkey()], - gas: 'auto', - chainId: 1313161554, - verify: { - etherscan: { - apiKey: etherscanKey('aurora'), - }, - }, - }, - bsc: { - live: true, - url: nodeUrl('bsc'), - accounts: [getPkey()], - gas: 60000000, - gasMultiplier: 3, - gasPrice: 'auto', - chainId: 56, - verify: { - etherscan: { - apiKey: etherscanKey('bsc'), - }, - }, - }, - gnosis: { - live: true, - url: nodeUrl('gnosis'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 3, - chainId: 100, - initialBaseFeePerGas: 2000000000, - verify: { - etherscan: { - apiKey: etherscanKey('gnosis'), - }, - }, - }, - polygonzkevm: { - live: true, - url: nodeUrl('polygonzkevm'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 1101, - verify: { - etherscan: { - apiKey: etherscanKey('polygonzkevm'), - }, - }, - }, - base: { - live: true, - url: nodeUrl('base'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 8453, - verify: { - etherscan: { - apiKey: etherscanKey('base'), - }, - }, - }, - bob: { - live: true, - url: nodeUrl('bob'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 60808, - verify: { - etherscan: { - apiKey: etherscanKey('bob'), - }, - }, - }, - linea: { - live: true, - url: nodeUrl('linea'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 59144, - verify: { - etherscan: { - apiKey: etherscanKey('linea'), - }, - }, - }, - zksync: { - live: true, - url: nodeUrl('zksync'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 324, - ethNetwork: nodeUrl('mainnet'), - verifyURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification', - verify: { - etherscan: { - apiKey: etherscanKey('zksync'), - }, - }, - zksync: true, - }, - mantle: { - live: true, - url: nodeUrl('mantle'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 5000, - verify: { - etherscan: { - apiKey: etherscanKey('mantle'), - }, - }, - }, - filecoin: { - live: true, - url: nodeUrl('filecoin'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 314, - verify: { - etherscan: { - apiKey: etherscanKey('filecoin'), - }, - }, - }, - blast: { - live: true, - url: nodeUrl('blast'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 81457, - verify: { - etherscan: { - apiKey: etherscanKey('blast'), - }, - }, - }, - mode: { - live: true, - url: nodeUrl('mode'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 34443, - verify: { - etherscan: { - apiKey: etherscanKey('mode'), - }, - }, - }, - thundercore: { - live: true, - url: nodeUrl('thundercore'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 108, - verify: { - etherscan: { - apiKey: etherscanKey('thundercore'), - }, - }, - }, - coredao: { - live: true, - url: nodeUrl('coredao'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 1116, - verify: { - etherscan: { - apiKey: etherscanKey('coredao'), - }, - }, - }, - xlayer: { - live: true, - url: nodeUrl('xlayer'), - accounts: [getPkey()], - gas: 'auto', - chainId: 196, - }, - taiko: { - live: true, - url: nodeUrl('taiko'), - accounts: [getPkey()], - gas: 'auto', - chainId: 167000, - }, - fuse: { - live: true, - url: nodeUrl('fuse'), - accounts: [getPkey()], - gas: 'auto', - chainId: 122, - }, - immutablezkevm: { - live: true, - url: nodeUrl('immutablezkevm'), - accounts: [getPkey()], - gas: 'auto', - gasPrice: 'auto', - gasMultiplier: 1.3, - chainId: 13371, - verify: { - etherscan: { - apiKey: etherscanKey('immutablezkevm'), - }, - }, - }, - immutable: { - live: true, - url: nodeUrl('immutable'), - accounts: [getPkey()], - gas: 'auto', - chainId: 13371, - verify: { - etherscan: { - apiKey: etherscanKey('immutable'), - }, - }, - }, - scroll: { - live: true, - url: nodeUrl('scroll'), - accounts: [getPkey()], - gas: 'auto', - chainId: 534352, - verify: { - etherscan: { - apiKey: etherscanKey('scroll'), - }, - }, - }, - manta: { - live: true, - url: nodeUrl('manta'), - accounts: [getPkey()], - gas: 'auto', - chainId: 169, - verify: { - etherscan: { - apiKey: etherscanKey('manta'), - }, - }, - }, - sei: { - live: true, - url: nodeUrl('sei'), - accounts: [getPkey()], - gas: 'auto', - chainId: 1329, - verify: { - etherscan: { - apiKey: etherscanKey('sei'), - }, - }, - }, - celo: { - live: true, - url: nodeUrl('celo'), - accounts: [getPkey()], - gas: 'auto', - chainId: 42220, - verify: { - etherscan: { - apiKey: etherscanKey('celo'), - }, - }, - }, - fraxtal: { - live: true, - url: nodeUrl('fraxtal'), - accounts: accountsMerklDeployer, - gas: 'auto', - chainId: 252, - verify: { - etherscan: { - apiKey: etherscanKey('fraxtal'), - }, - }, - }, - astar: { - live: true, - url: nodeUrl('astar'), - accounts: [getPkey()], - gas: 'auto', - gasPrice: 'auto', - chainId: 592, - verify: { - etherscan: { - apiKey: etherscanKey('astar'), - }, - }, - }, - astarzkevm: { - live: true, - url: nodeUrl('astarzkevm'), - accounts: [getPkey()], - gas: 'auto', - gasPrice: 'auto', - chainId: 3776, - verify: { - etherscan: { - apiKey: etherscanKey('astarzkevm'), - }, - }, - }, - rootstock: { - live: true, - url: nodeUrl('rootstock'), - accounts: accounts('rootstock'), - gas: 'auto', - chainId: 30, - verify: { - etherscan: { - apiKey: etherscanKey('rootstock'), - }, - }, - }, - moonbeam: { - live: true, - url: nodeUrl('moonbeam'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 1284, - verify: { - etherscan: { - apiKey: etherscanKey('moonbeam'), - }, - }, - }, - skale: { - live: true, - url: nodeUrl('skale'), - accounts: [getPkey()], - gas: 'auto', - gasMultiplier: 1.3, - chainId: 2046399126, - verify: { - etherscan: { - apiKey: etherscanKey('skale'), - }, - }, - }, - worldchain: { - live: true, - url: nodeUrl('worldchain'), - accounts: accountsMerklDeployer, - gas: 'auto', - chainId: 480, - verify: { - etherscan: { - apiKey: etherscanKey('worldchain'), - }, - }, - }, - }, - paths: { - sources: './contracts', - tests: './test', - cache: 'cache-hh', - }, - namedAccounts: { - deployer: 0, - guardian: 1, - governor: 2, - proxyAdmin: 3, - alice: 4, - bob: 5, - charlie: 6, - }, - contractSizer: { - alphaSort: true, - runOnCompile: false, - disambiguatePaths: false, - }, - gasReporter: { - currency: 'USD', - outputFile: 'gas-report.txt', - }, - spdxLicenseIdentifier: { - overwrite: false, - runOnCompile: true, - }, - abiExporter: { - path: './export/abi', - clear: true, - flat: true, - spacing: 2, - }, - etherscan: { - // apiKey: process.env.ETHERSCAN_API_KEY, - apiKey: { - worldchain: etherscanKey('worldchain'), - }, - customChains: [ - { - network: 'taiko', - chainId: 167000, - urls: { - apiURL: 'https://api.taikoscan.io/api', - browserURL: 'https://taikoscan.io/', - }, - }, - { - network: 'celo', - chainId: 42220, - urls: { - apiURL: 'https://api.celoscan.io/api', - browserURL: 'https://celoscan.io/', - }, - }, - { - network: 'fraxtal', - chainId: 252, - urls: { - apiURL: 'https://api.fraxscan.com/api', - browserURL: 'https://fraxscan.com/', - }, - }, - { - network: 'skale', - chainId: 2046399126, - urls: { - apiURL: 'https://internal-hubs.explorer.mainnet.skalenodes.com:10001/api', - browserURL: 'https://elated-tan-skat.explorer.mainnet.skalenodes.com/', - }, - }, - { - network: 'worldchain', - chainId: 480, - urls: { - apiURL: 'https://worldchain-mainnet.explorer.alchemy.com/api', - browserURL: 'https://worldchain-mainnet.explorer.alchemy.com/', - }, - }, - ], - }, - typechain: { - outDir: 'typechain', - target: 'ethers-v5', - }, -}; - -export default config; From 09efca13c51be49477b63f47f65d4a5966861959 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 16:30:00 +0100 Subject: [PATCH 03/13] fix: puffer wrapper --- LICENSE | 2 +- .../tokenWrappers/PufferPointTokenWrapper.sol | 83 ++++++++++++++----- .../partners/tokenWrappers/SonicFragment.sol | 71 ---------------- scripts/deployPufferPointTokenWrapper.s.sol | 39 +++++++++ 4 files changed, 102 insertions(+), 93 deletions(-) delete mode 100644 contracts/partners/tokenWrappers/SonicFragment.sol create mode 100644 scripts/deployPufferPointTokenWrapper.s.sol diff --git a/LICENSE b/LICENSE index 67471c4..9fd406f 100644 --- a/LICENSE +++ b/LICENSE @@ -10,7 +10,7 @@ Parameters Licensor: Angle Labs, Inc. Licensed Work: Merkl Smart Contracts -The Licensed Work is (c) 2024 Angle Labs, Inc. +The Licensed Work is (c) 2025 Angle Labs, Inc. Additional Use Grant: Any uses listed and defined at merkl-license-grants.angle-labs.eth diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol index 3352e2c..f9c70c1 100644 --- a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -6,10 +6,10 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol"; +import { IAccessControlManager } from "./BaseTokenWrapper.sol"; -import { UUPSHelper } from "../../utils/UUPSHelper.sol"; -import { Errors } from "../../utils/Errors.sol"; +import "../../utils/UUPSHelper.sol"; +import "../../utils/Errors.sol"; struct VestingID { uint128 amount; @@ -38,8 +38,8 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice `AccessControlManager` contract handling access control - IAccessControlManager public accessControlManager; + /// @notice `Core` contract handling access control + IAccessControlManager public core; /// @notice Merkl main functions address public distributor; address public feeRecipient; @@ -53,13 +53,16 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { mapping(address => VestingData) public vestingData; event Recovered(address indexed token, address indexed to, uint256 amount); + event MerklAddressesUpdated(address indexed _distributionCreator, address indexed _distributor); + event CliffDurationUpdated(uint32 _newCliffDuration); + event FeeRecipientUpdated(address indexed _feeRecipient); // ================================= FUNCTIONS ================================= function initialize( address _underlying, uint32 _cliffDuration, - IAccessControlManager _accessControlManager, + IAccessControlManager _core, address _distributionCreator ) public initializer { __ERC20_init( @@ -67,10 +70,11 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { string.concat("mtw", IERC20Metadata(_underlying).symbol()) ); __UUPSUpgradeable_init(); - if (address(_accessControlManager) == address(0)) revert Errors.ZeroAddress(); + if (address(_core) == address(0)) revert Errors.ZeroAddress(); underlying = _underlying; - accessControlManager = _accessControlManager; + core = _core; cliffDuration = _cliffDuration; + distributionCreator = _distributionCreator; distributor = IDistributionCreator(_distributionCreator).distributor(); feeRecipient = IDistributionCreator(_distributionCreator).feeRecipient(); } @@ -105,15 +109,24 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { if (from == distributor) { _burn(to, amount); - // Creates a vesting for the `to` address - VestingData storage userVestingData = vestingData[to]; - VestingID[] storage userAllVestings = userVestingData.allVestings; - userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration))); + uint128 endTimestamp = uint128(block.timestamp + cliffDuration); + if (endTimestamp > block.timestamp) { + // Creates a vesting for the `to` address + VestingData storage userVestingData = vestingData[to]; + VestingID[] storage userAllVestings = userVestingData.allVestings; + userAllVestings.push(VestingID(uint128(amount), uint128(block.timestamp + cliffDuration))); + } else { + IERC20(token()).safeTransfer(to, amount); + } } } function claim(address user) external returns (uint256) { - (uint256 claimed, uint256 nextClaimIndex) = _claimable(user); + return claim(user, type(uint256).max); + } + + function claim(address user, uint256 maxClaimIndex) public returns (uint256) { + (uint256 claimed, uint256 nextClaimIndex) = _claimable(user, maxClaimIndex); if (claimed > 0) { vestingData[user].nextClaimIndex = nextClaimIndex; IERC20(token()).safeTransfer(user, claimed); @@ -122,7 +135,11 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { } function claimable(address user) external view returns (uint256 amountClaimable) { - (amountClaimable, ) = _claimable(user); + return claimable(user, type(uint256).max); + } + + function claimable(address user, uint256 maxClaimIndex) public view returns (uint256 amountClaimable) { + (amountClaimable, ) = _claimable(user, maxClaimIndex); } function getUserVestings( @@ -133,12 +150,15 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { nextClaimIndex = userVestingData.nextClaimIndex; } - function _claimable(address user) internal view returns (uint256 amountClaimable, uint256 nextClaimIndex) { + function _claimable( + address user, + uint256 maxClaimIndex + ) internal view returns (uint256 amountClaimable, uint256 nextClaimIndex) { VestingData storage userVestingData = vestingData[user]; VestingID[] storage userAllVestings = userVestingData.allVestings; uint256 i = userVestingData.nextClaimIndex; uint256 length = userAllVestings.length; - while (i < length) { + while (i < length && i <= maxClaimIndex) { VestingID storage userCurrentVesting = userAllVestings[i]; if (block.timestamp > userCurrentVesting.unlockTimestamp) { amountClaimable += userCurrentVesting.amount; @@ -153,12 +173,18 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { /// @notice Checks whether the `msg.sender` has the governor role or the guardian role modifier onlyGovernor() { - if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotGovernor(); + if (!core.isGovernor(msg.sender)) revert Errors.NotGovernor(); + _; + } + + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier onlyGuardian() { + if (!core.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian(); _; } - /// @inheritdoc UUPSHelper - function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(accessControlManager) {} + /// @inheritdoc UUPSUpgradeable + function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} /// @notice Recovers any ERC20 token /// @dev Governance only, to trigger only if something went wrong @@ -168,11 +194,26 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { } function setDistributor(address _distributionCreator) external onlyGovernor { - distributor = IDistributionCreator(_distributionCreator).distributor(); + address _distributor = IDistributionCreator(_distributionCreator).distributor(); + distributor = _distributor; distributionCreator = _distributionCreator; + emit MerklAddressesUpdated(_distributionCreator, _distributor); + _setFeeRecipient(); + } + + function setCliffDuration(uint32 _newCliffDuration) external onlyGuardian { + if (_newCliffDuration < cliffDuration && _newCliffDuration != 0) revert Errors.InvalidParam(); + cliffDuration = _newCliffDuration; + emit CliffDurationUpdated(_newCliffDuration); } function setFeeRecipient() external { - feeRecipient = IDistributionCreator(distributionCreator).feeRecipient(); + _setFeeRecipient(); + } + + function _setFeeRecipient() internal { + address _feeRecipient = IDistributionCreator(distributionCreator).feeRecipient(); + feeRecipient = _feeRecipient; + emit FeeRecipientUpdated(_feeRecipient); } } diff --git a/contracts/partners/tokenWrappers/SonicFragment.sol b/contracts/partners/tokenWrappers/SonicFragment.sol deleted file mode 100644 index 7e7ebbd..0000000 --- a/contracts/partners/tokenWrappers/SonicFragment.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.17; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; - -import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol"; -import { Errors } from "../../utils/Errors.sol"; - -/// @title SonicFragment -/// @author Angle Labs, Inc. -contract SonicFragment is ERC2O { - using SafeERC20 for IERC20; - - /// @notice `AccessControlManager` contract handling access control - IAccessControlManager public accessControlManager; - uint256 public exchangeRate; - address public sToken; - uint8 public contractSettled; - - constructor( - address _accessControlManager, - address recipient, - uint256 _totalSupply, - string memory _name, - string memory _symbol - ) ERC20(_name, _symbol) { - // Zero address check - IAccessControlManager(_accessControlManager).isGovernor(msg.sender); - accessControlManager = IAccessControlManager(_accessControlManager); - _mint(recipient, _totalSupply); - } - - // ================================= MODIFIERS ================================= - - /// @notice Checks whether the `msg.sender` has the governor role or the guardian role - modifier isGovernor() { - if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotAllowed(); - _; - } - - /// @notice Activates the contract settlement and enables redemption of fragments into S - function settleContract(uint256 sTokenAmount) external isGovernor { - if (contractSettled > 0) revert Errors.NotAllowed(); - IERC20(sToken).safeTransferFrom(msg.sender, address(this), sTokenAmount); - contractSettled = 1; - uint256 _totalSupply = totalSupply(); - exchangeRate = (sTokenAmount * 1 ether) / _totalSupply; - } - - /// @notice Sets the S address - /// @dev Cannot be set once redemption is activated - function setSTokenAddress(address sTokenAddress) external isGovernor { - if (contractSettled == 0) sToken = sTokenAddress; - } - - /// @notice Redeems fragments against S - function redeem(uint256 amount, address recipient) external { - _burn(msg.sender, amount); - uint256 amountToSend = (amount * exchangeRate) / 1 ether; - IERC20(sToken).safeTransfer(recipient, amount); - } - - /// @notice Recovers leftover tokens after sometime - function recover(uint256 amount, address recipient) external onlyGovernor { - IERC20(sToken).safeTransfer(recipient, amount); - exchangeRate = 0; - } -} diff --git a/scripts/deployPufferPointTokenWrapper.s.sol b/scripts/deployPufferPointTokenWrapper.s.sol new file mode 100644 index 0000000..a33bc15 --- /dev/null +++ b/scripts/deployPufferPointTokenWrapper.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; + +import { BaseScript } from "./utils/Base.s.sol"; + +import { console } from "forge-std/console.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ITransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { JsonReader } from "@utils/JsonReader.sol"; +import { ContractType } from "@utils/Constants.sol"; + +import { PufferPointTokenWrapper } from "../contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol"; +import { DistributionCreator } from "../contracts/DistributionCreator.sol"; +import { IAccessControlManager } from "../contracts/interfaces/IAccessControlManager.sol"; +import { MockToken } from "../contracts/mock/MockToken.sol"; + +contract DeployPufferPointTokenWrapper is BaseScript { + function run() public broadcast { + console.log("DEPLOYER_ADDRESS:", broadcaster); + address underlying = 0x282A69142bac47855C3fbE1693FcC4bA3B4d5Ed6; + uint32 cliffDuration = 1 weeks; + IAccessControlManager manager = IAccessControlManager(0x0E632a15EbCBa463151B5367B4fCF91313e389a6); + address distributionCreator = 0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd; + + // Deploy implementation + PufferPointTokenWrapper implementation = new PufferPointTokenWrapper(); + console.log("PufferPointTokenWrapper Implementation:", address(implementation)); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), ""); + console.log("PufferPointTokenWrapper Proxy:", address(proxy)); + + // Initialize + PufferPointTokenWrapper(address(proxy)).initialize(underlying, cliffDuration, manager, distributionCreator); + } +} From c83e6281a235927d2ba8fd6f86aa550f0d9f23bf Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 16:33:40 +0100 Subject: [PATCH 04/13] fix: bun std --- lib/forge-std | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lib/forge-std diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index cb69e9c..0000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cb69e9c07fbd002819c8c6c8db3caeab76b90d6b From 430e054609459fc1f954c2f4f4b54e2ab2792ab9 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 16:34:58 +0100 Subject: [PATCH 05/13] feat: add sonic fragment --- .../tokenWrappers/PufferPointTokenWrapper.sol | 6 +- .../partners/tokenWrappers/SonicFragment.sol | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 contracts/partners/tokenWrappers/SonicFragment.sol diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol index f9c70c1..f1955a0 100644 --- a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -62,7 +62,7 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { function initialize( address _underlying, uint32 _cliffDuration, - IAccessControlManager _core, + IAccessControlManager _accessControlManager, address _distributionCreator ) public initializer { __ERC20_init( @@ -70,9 +70,9 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { string.concat("mtw", IERC20Metadata(_underlying).symbol()) ); __UUPSUpgradeable_init(); - if (address(_core) == address(0)) revert Errors.ZeroAddress(); + if (address(_accessControlManager) == address(0)) revert Errors.ZeroAddress(); underlying = _underlying; - core = _core; + core = _accessControlManager; cliffDuration = _cliffDuration; distributionCreator = _distributionCreator; distributor = IDistributionCreator(_distributionCreator).distributor(); diff --git a/contracts/partners/tokenWrappers/SonicFragment.sol b/contracts/partners/tokenWrappers/SonicFragment.sol new file mode 100644 index 0000000..12aa035 --- /dev/null +++ b/contracts/partners/tokenWrappers/SonicFragment.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.17; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol"; +import { Errors } from "../../utils/Errors.sol"; + +/// @title SonicFragment +/// @notice Contract for Sonic fragments which can be converted upon activation into S tokens +/// @author Angle Labs, Inc. +contract SonicFragment is ERC20 { + using SafeERC20 for IERC20; + + /// @notice Contract handling access control + IAccessControlManager public immutable accessControlManager; + /// @notice Address for the S token + address public immutable sToken; + + /// @notice Amount of S tokens sent on the contract at the activation of redemption + /// @dev Used to compute the exchange rate between fragments and S tokens + uint128 public sTokenAmount; + /// @notice Total supply of the contract + /// @dev Needs to be stored to compute the exchange rate between fragments and sTokens + uint120 public supply; + /// @notice Whether redemption for S tokens has been activated or not + uint8 public contractSettled; + + constructor( + address _accessControlManager, + address recipient, + address _sToken, + uint256 _totalSupply, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + // Zero address check + if (_sToken == address(0)) revert Errors.ZeroAddress(); + IAccessControlManager(_accessControlManager).isGovernor(msg.sender); + sToken = _sToken; + accessControlManager = IAccessControlManager(_accessControlManager); + supply = uint120(_totalSupply); + _mint(recipient, _totalSupply); + } + + // ================================= MODIFIERS ================================= + + /// @notice Checks whether the `msg.sender` has the governor role + modifier onlyGovernor() { + if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotAllowed(); + _; + } + + /// @notice Activates the contract settlement and enables redemption of fragments into S + /// @dev Can only be called once + function settleContract(uint256 _sTokenAmount) external onlyGovernor { + if (contractSettled > 0) revert Errors.NotAllowed(); + contractSettled = 1; + IERC20(sToken).safeTransferFrom(msg.sender, address(this), sTokenAmount); + sTokenAmount = uint128(_sTokenAmount); + } + + /// @notice Recovers leftover tokens after sometime + function recover(uint256 amount, address recipient) external onlyGovernor { + IERC20(sToken).safeTransfer(recipient, amount); + sTokenAmount = 0; + } + + /// @notice Redeems fragments against S based on a predefined exchange rate + function redeem(uint256 amount, address recipient) external returns (uint256 amountToSend) { + uint128 _sTokenAmount = sTokenAmount; + if (_sTokenAmount == 0) revert Errors.NotAllowed(); + _burn(msg.sender, amount); + amountToSend = (amount * _sTokenAmount) / supply; + IERC20(sToken).safeTransfer(recipient, amountToSend); + } +} From 8da1686e45c470320010682bd8703f3bae977489 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 16:51:44 +0100 Subject: [PATCH 06/13] feat: fragment --- scripts/deploySonicFragment.s.sol | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 scripts/deploySonicFragment.s.sol diff --git a/scripts/deploySonicFragment.s.sol b/scripts/deploySonicFragment.s.sol new file mode 100644 index 0000000..51bc795 --- /dev/null +++ b/scripts/deploySonicFragment.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import { console } from "forge-std/console.sol"; + +import { BaseScript } from "./utils/Base.s.sol"; + +import { console } from "forge-std/console.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ITransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { JsonReader } from "@utils/JsonReader.sol"; +import { ContractType } from "@utils/Constants.sol"; + +import { SonicFragment } from "../contracts/partners/tokenWrappers/SonicFragment.sol"; +import { DistributionCreator } from "../contracts/DistributionCreator.sol"; +import { IAccessControlManager } from "../contracts/interfaces/IAccessControlManager.sol"; +import { MockToken } from "../contracts/mock/MockToken.sol"; + +contract DeploySonicFragment is BaseScript { + function run() public broadcast { + console.log("DEPLOYER_ADDRESS:", broadcaster); + + // Sonic address - to check + IAccessControlManager manager = IAccessControlManager(0xa25c30044142d2fA243E7Fd3a6a9713117b3c396); + address recipient = address(broadcaster); + // TODO this is the wrapped Sonic address + address sToken = address(0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38); + uint256 totalSupply = 100_000_000 ether; + string memory name = "Fragment xxx"; + string memory symbol = "frgxxx"; + + // Deploy implementation + SonicFragment implementation = new SonicFragment( + address(manager), + recipient, + sToken, + totalSupply, + name, + symbol + ); + console.log("SonicFragment deployed at:", address(implementation)); + } +} From 742a69bea908613926938f112be03aa472eb93bf Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 16:59:53 +0100 Subject: [PATCH 07/13] feat: point token --- README.md | 2 + .../partners/tokenWrappers/PointToken.sol | 75 +++++++++++++++++++ docs/access_control.svg | 11 +++ 3 files changed, 88 insertions(+) create mode 100644 contracts/partners/tokenWrappers/PointToken.sol create mode 100644 docs/access_control.svg diff --git a/README.md b/README.md index ede2730..2d556aa 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ function run() external broadcast { The Merkl smart contracts have been audited by Code4rena, find the audit report [here](https://code4rena.com/reports/2023-06-angle). +## Access Control + ## Media Don't hesitate to reach out on [Twitter](https://x.com/merkl_xyz) 🐦 diff --git a/contracts/partners/tokenWrappers/PointToken.sol b/contracts/partners/tokenWrappers/PointToken.sol new file mode 100644 index 0000000..7d2005a --- /dev/null +++ b/contracts/partners/tokenWrappers/PointToken.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IAccessControlManager } from "../../interfaces/IAccessControlManager.sol"; +import "../../utils/Errors.sol"; + +/// @title PointToken +/// @author Angle Labs, Inc. +/// @notice Reference contract for points systems within Merkl +contract PointToken is ERC20 { + mapping(address => bool) public minters; + mapping(address => bool) public whitelistedRecipients; + IAccessControlManager public accessControlManager; + uint8 public allowedTransfers; + + constructor( + string memory name_, + string memory symbol_, + address _minter, + address _accessControlManager + ) ERC20(name_, symbol_) { + if (_accessControlManager == address(0) || _minter == address(0)) revert Errors.ZeroAddress(); + accessControlManager = IAccessControlManager(_accessControlManager); + minters[_minter] = true; + } + + modifier onlyGovernorOrGuardian() { + if (!accessControlManager.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian(); + _; + } + + modifier onlyMinter() { + if (!minters[msg.sender]) revert Errors.NotTrusted(); + _; + } + + function mint(address account, uint256 amount) external onlyMinter { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external onlyMinter { + _burn(account, amount); + } + + function mintBatch(address[] memory accounts, uint256[] memory amounts) external onlyMinter { + uint256 length = accounts.length; + for (uint256 i = 0; i < length; ++i) { + _mint(accounts[i], amounts[i]); + } + } + + function toggleMinter(address minter) external onlyGovernorOrGuardian { + minters[minter] = !minters[minter]; + } + + function toggleAllowedTransfers() external onlyGovernorOrGuardian { + allowedTransfers = 1 - allowedTransfers; + } + + function toggleWhitelistedRecipient(address recipient) external onlyGovernorOrGuardian { + whitelistedRecipients[recipient] = !whitelistedRecipients[recipient]; + } + + function _beforeTokenTransfer(address from, address to, uint256) internal view override { + if ( + allowedTransfers == 0 && + from != address(0) && + to != address(0) && + !whitelistedRecipients[from] && + !whitelistedRecipients[to] + ) revert Errors.NotAllowed(); + } +} diff --git a/docs/access_control.svg b/docs/access_control.svg new file mode 100644 index 0000000..06b9d3d --- /dev/null +++ b/docs/access_control.svg @@ -0,0 +1,11 @@ + + + + + + + + ContractsRolesActorsAngleLabsMultisigDeployerEOAUsersWhitelistedOperatorsWhitelistedDisputers2-of-3 Multisig0x8f..02b40xf8..c3490x34..c41dGUARDIANGOVERNORCoreDistributorDistributorCreatorDisputersetRolesetRolehasRolehasRolehasRoleupgrade[core.onlyGovernor]upgrade[core.onlyGovernor]updateTree[canUpdateMerkleTreeor core.governor]resolveDispute[core.onlyGovernor]ownsupdateTree[canUpdateMerkleTreeor core.governor]claim[public]disputeTree[public (100 EURA)]create[public]claim[onlyOperator]toggleDispute[disputer.onlyWhitelisted]disputeTree[public (100 EURA)] \ No newline at end of file From 35333dcfe57581032b77e32776332c57de51720a Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 17:02:38 +0100 Subject: [PATCH 08/13] fix: tests --- .../pufferPointTokenWrapper.test.ts | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 test/deprecated/hardhat/tokenWrappers/pufferPointTokenWrapper.test.ts diff --git a/test/deprecated/hardhat/tokenWrappers/pufferPointTokenWrapper.test.ts b/test/deprecated/hardhat/tokenWrappers/pufferPointTokenWrapper.test.ts new file mode 100644 index 0000000..cd04a69 --- /dev/null +++ b/test/deprecated/hardhat/tokenWrappers/pufferPointTokenWrapper.test.ts @@ -0,0 +1,248 @@ +/* +// TODO: write tests back in Foundry +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { parseEther, solidityKeccak256 } from 'ethers/lib/utils'; +import hre, { contract, ethers, web3,network } from 'hardhat'; +import { Signer } from 'ethers'; + +import { + DistributionCreator, + DistributionCreator__factory, + MockCoreBorrow, + MockCoreBorrow__factory, + MockToken, + MockToken__factory, + MockUniswapV3Pool, + MockUniswapV3Pool__factory, + PufferPointTokenWrapper, + PufferPointTokenWrapper__factory +} from '../../../typechain'; +import { parseAmount } from '../../../utils/bignumber'; +import { inReceipt } from '../utils/expectEvent'; +import { deployUpgradeableUUPS, increaseTime, latestTime, MAX_UINT256, ZERO_ADDRESS } from '../utils/helpers'; + +contract('PufferPointTokenWrapper', () => { + let deployer: SignerWithAddress; + let alice: SignerWithAddress; + let bob: SignerWithAddress; + let governor: string; + let guardian: string; + let distributor: string; + let distributionCreator: string; + let feeRecipient: string; + let angle: MockToken; + let agEUR: MockToken; + let tokenWrapper: PufferPointTokenWrapper; + let cliffDuration: number; + + let manager: DistributionCreator; + let core: MockCoreBorrow; + + const impersonatedSigners: { [key: string]: Signer } = {}; + + beforeEach(async () => { + [deployer, alice, bob] = await ethers.getSigners(); + await network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: process.env.ETH_NODE_URI_MAINNET, + blockNumber: 21313975, + }, + }, + ], + }); + // add any addresses you want to impersonate here + governor = '0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8'; + guardian = '0x0C2553e4B9dFA9f83b1A6D3EAB96c4bAaB42d430'; + distributor = '0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae'; + distributionCreator = '0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd'; + feeRecipient = '0xeaC6A75e19beB1283352d24c0311De865a867DAB' + const impersonatedAddresses = [governor, guardian, distributor, distributionCreator, feeRecipient]; + + for (const address of impersonatedAddresses) { + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [address], + }); + await hre.network.provider.send('hardhat_setBalance', [address, '0x10000000000000000000000000000']); + impersonatedSigners[address] = await ethers.getSigner(address); + } + + angle = (await new MockToken__factory(deployer).deploy('ANGLE', 'ANGLE', 18)) as MockToken; + core = (await new MockCoreBorrow__factory(deployer).deploy()) as MockCoreBorrow; + await core.toggleGuardian(guardian); + await core.toggleGovernor(governor); + cliffDuration = 2592000; + + tokenWrapper = (await deployUpgradeableUUPS(new PufferPointTokenWrapper__factory(deployer))) as PufferPointTokenWrapper; + await tokenWrapper.initialize(angle.address, cliffDuration, core.address, distributionCreator); + await angle.mint(alice.address, parseEther('1000')); + }); + describe('upgrade', () => { + it('success - upgrades to new implementation', async () => { + const newImplementation = await new PufferPointTokenWrapper__factory(deployer).deploy(); + await tokenWrapper.connect(impersonatedSigners[governor]).upgradeTo(newImplementation.address); + }); + it('reverts - when called by unallowed address', async () => { + const newImplementation = await new PufferPointTokenWrapper__factory(deployer).deploy(); + await expect(tokenWrapper.connect(alice).upgradeTo(newImplementation.address)).to.be.revertedWithCustomError( + tokenWrapper, + 'NotGovernor', + ); + }); + }); + describe('initializer', () => { + it('success - treasury', async () => { + expect(await tokenWrapper.cliffDuration()).to.be.equal(cliffDuration); + expect(await tokenWrapper.core()).to.be.equal(core.address); + expect(await tokenWrapper.underlying()).to.be.equal(angle.address); + }); + it('reverts - already initialized', async () => { + await expect(tokenWrapper.initialize(angle.address, cliffDuration,core.address, distributionCreator)).to.be.revertedWith( + 'Initializable: contract is already initialized', + ); + }); + it('reverts - zero address', async () => { + const tokenWrapperRevert = (await deployUpgradeableUUPS(new PufferPointTokenWrapper__factory(deployer))) as PufferPointTokenWrapper; + await expect( + tokenWrapperRevert.initialize(ZERO_ADDRESS,cliffDuration, core.address, distributionCreator), + ).to.be.reverted; + }); + }); + describe('createCampaign', () => { + it('success - balance credited', async () => { + await angle.connect(alice).approve(tokenWrapper.address, MAX_UINT256); + await tokenWrapper.connect(alice).transfer(distributor,parseEther('1')); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('1')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('1')); + }); + it('success - balance credited - feeRecipient', async () => { + await angle.connect(alice).approve(tokenWrapper.address, MAX_UINT256); + await tokenWrapper.connect(alice).transfer(feeRecipient,parseEther('1')); + expect(await tokenWrapper.balanceOf(feeRecipient)).to.be.equal(0); + expect(await angle.balanceOf(feeRecipient)).to.be.equal(parseEther('1')); + }); + it('reverts - when other contract', async () => { + await angle.connect(alice).approve(tokenWrapper.address, MAX_UINT256); + await expect(tokenWrapper.connect(alice).transfer(governor,parseEther('1'))).to.be.reverted; + }); + }); + describe('claimRewards', () => { + it('success - balance credited', async () => { + await angle.connect(alice).approve(tokenWrapper.address, MAX_UINT256); + await tokenWrapper.connect(alice).transfer(distributor,parseEther('1')); + await tokenWrapper.connect(impersonatedSigners[distributor]).transfer(bob.address, parseEther('0.5')); + const endData = await latestTime(); + expect(await tokenWrapper.balanceOf(bob.address)).to.be.equal(0); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.5')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('1')); + + const vestings = await tokenWrapper.getUserVestings(bob.address); + expect(vestings[0][0].amount).to.be.equal(parseEther('0.5')); + expect(vestings[0][0].unlockTimestamp).to.be.equal(endData+cliffDuration); + expect(vestings[1]).to.be.equal(0); + + await increaseTime(cliffDuration/2); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(0); + await increaseTime(cliffDuration*2); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(parseEther('0.5')); + await tokenWrapper.claim(bob.address); + expect(await angle.balanceOf(bob.address)).to.be.equal(parseEther('0.5')) + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.5')); + + const vestings2 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings2[0][0].amount).to.be.equal(parseEther('0.5')); + expect(vestings2[0][0].unlockTimestamp).to.be.equal(endData+cliffDuration); + expect(vestings2[1]).to.be.equal(1); + + await tokenWrapper.connect(impersonatedSigners[distributor]).transfer(bob.address, parseEther('0.2')); + const endTime2 = await latestTime(); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.3')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.5')); + const vestings3 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings3[0][1].amount).to.be.equal(parseEther('0.2')); + expect(vestings3[0][1].unlockTimestamp).to.be.equal(endTime2+cliffDuration); + expect(vestings3[1]).to.be.equal(1); + + await increaseTime(cliffDuration/2); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(0); + await tokenWrapper.connect(impersonatedSigners[distributor]).transfer(bob.address, parseEther('0.12')); + const endTime3 = await latestTime(); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.18')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.5')); + + const vestings4 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings4[0][1].amount).to.be.equal(parseEther('0.2')); + expect(vestings4[0][1].unlockTimestamp).to.be.equal(endTime2+cliffDuration); + expect(vestings4[1]).to.be.equal(1); + expect(vestings4[0][2].amount).to.be.equal(parseEther('0.12')); + expect(vestings4[0][2].unlockTimestamp).to.be.equal(endTime3+cliffDuration); + + await increaseTime(cliffDuration*3/4); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(parseEther('0.2')); + + await tokenWrapper.claim(bob.address) + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(parseEther('0')); + expect(await angle.balanceOf(bob.address)).to.be.equal(parseEther('0.7')) + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.3')); + + const vestings5 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings5[0][1].amount).to.be.equal(parseEther('0.2')); + expect(vestings5[0][1].unlockTimestamp).to.be.equal(endTime2+cliffDuration); + expect(vestings5[1]).to.be.equal(2); + + await tokenWrapper.connect(impersonatedSigners[distributor]).transfer(bob.address, parseEther('0.1')); + const endTime4 = await latestTime(); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.08')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.3')); + const vestings6 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings6[0][3].amount).to.be.equal(parseEther('0.1')); + expect(vestings6[0][3].unlockTimestamp).to.be.equal(endTime4+cliffDuration); + expect(vestings6[1]).to.be.equal(2); + + await tokenWrapper.connect(impersonatedSigners[distributor]).transfer(alice.address, parseEther('0.05')); + const endTime5 = await latestTime(); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.3')); + const vestings7 = await tokenWrapper.getUserVestings(alice.address); + expect(vestings7[0][0].amount).to.be.equal(parseEther('0.05')); + expect(vestings7[0][0].unlockTimestamp).to.be.equal(endTime5+cliffDuration); + expect(vestings7[1]).to.be.equal(0); + + await increaseTime(cliffDuration*2); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(parseEther('0.22')); + expect(await tokenWrapper.claimable(alice.address)).to.be.equal(parseEther('0.05')); + await tokenWrapper.claim(bob.address); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.08')); + expect(await angle.balanceOf(bob.address)).to.be.equal(parseEther('0.92')); + + const vestings8 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings8[1]).to.be.equal(4); + expect(await tokenWrapper.claimable(bob.address)).to.be.equal(0); + + await tokenWrapper.claim(alice.address); + const vestings9 = await tokenWrapper.getUserVestings(alice.address); + expect(vestings9[1]).to.be.equal(1); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(bob.address)).to.be.equal(parseEther('0.92')); + expect(await angle.balanceOf(alice.address)).to.be.equal(parseEther('999.05')); + + await tokenWrapper.claim(alice.address); + await tokenWrapper.claim(bob.address); + const vestings10 = await tokenWrapper.getUserVestings(alice.address); + const vestings11 = await tokenWrapper.getUserVestings(bob.address); + expect(vestings10[1]).to.be.equal(1); + expect(vestings11[1]).to.be.equal(4); + expect(await tokenWrapper.balanceOf(distributor)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(tokenWrapper.address)).to.be.equal(parseEther('0.03')); + expect(await angle.balanceOf(bob.address)).to.be.equal(parseEther('0.92')); + expect(await angle.balanceOf(alice.address)).to.be.equal(parseEther('999.05')); + }); + }); +}); +*/ \ No newline at end of file From d32d36de338edab12e3417621d369bcd3c382c4b Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 19:23:20 +0100 Subject: [PATCH 09/13] fix: access control schema --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2d556aa..083ee92 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ The Merkl smart contracts have been audited by Code4rena, find the audit report ## Access Control +![Access Control Schema](docs/access_control.svg) + ## Media Don't hesitate to reach out on [Twitter](https://x.com/merkl_xyz) 🐦 From 0f72685489b0f24ea24e493bdec5e8daf71859fe Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 14 Jan 2025 20:19:38 +0100 Subject: [PATCH 10/13] fix: wrapper --- .../tokenWrappers/PufferPointTokenWrapper.sol | 12 ++++++------ package.json | 2 +- scripts/deployPufferPointTokenWrapper.s.sol | 19 ++++++++++++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol index f1955a0..de9aac5 100644 --- a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -38,8 +38,8 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice `Core` contract handling access control - IAccessControlManager public core; + /// @notice `accessControlManager` contract handling access control + IAccessControlManager public accessControlManager; /// @notice Merkl main functions address public distributor; address public feeRecipient; @@ -72,7 +72,7 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { __UUPSUpgradeable_init(); if (address(_accessControlManager) == address(0)) revert Errors.ZeroAddress(); underlying = _underlying; - core = _accessControlManager; + accessControlManager = _accessControlManager; cliffDuration = _cliffDuration; distributionCreator = _distributionCreator; distributor = IDistributionCreator(_distributionCreator).distributor(); @@ -173,18 +173,18 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { /// @notice Checks whether the `msg.sender` has the governor role or the guardian role modifier onlyGovernor() { - if (!core.isGovernor(msg.sender)) revert Errors.NotGovernor(); + if (!accessControlManager.isGovernor(msg.sender)) revert Errors.NotGovernor(); _; } /// @notice Checks whether the `msg.sender` has the governor role or the guardian role modifier onlyGuardian() { - if (!core.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian(); + if (!accessControlManager.isGovernorOrGuardian(msg.sender)) revert Errors.NotGovernorOrGuardian(); _; } /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} + function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(accessControlManager) {} /// @notice Recovers any ERC20 token /// @dev Governance only, to trigger only if something went wrong diff --git a/package.json b/package.json index 2fc505b..36e1b16 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "foundry:compile": "forge build --optimize --optimizer-runs 1000", "foundry:coverage": "forge coverage --ir-minimum --report lcov && yarn lcov:clean && yarn lcov:generate-html", "foundry:script": "forge script -vvvv", - "foundry:deploy": "forge script --broadcast --verify -vvvv", + "foundry:deploy": "source .env && forge script --broadcast --verify -vvvv", "foundry:gas": "forge test --gas-report", "foundry:run": "docker run -it --rm -v $(pwd):/app -w /app ghcr.io/foundry-rs/foundry sh", "foundry:setup": "curl -L https://foundry.paradigm.xyz | bash && foundryup && git submodule update --init --recursive", diff --git a/scripts/deployPufferPointTokenWrapper.s.sol b/scripts/deployPufferPointTokenWrapper.s.sol index a33bc15..e079903 100644 --- a/scripts/deployPufferPointTokenWrapper.s.sol +++ b/scripts/deployPufferPointTokenWrapper.s.sol @@ -18,12 +18,24 @@ import { IAccessControlManager } from "../contracts/interfaces/IAccessControlMan import { MockToken } from "../contracts/mock/MockToken.sol"; contract DeployPufferPointTokenWrapper is BaseScript { - function run() public broadcast { - console.log("DEPLOYER_ADDRESS:", broadcaster); + function run() public { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + /* address underlying = 0x282A69142bac47855C3fbE1693FcC4bA3B4d5Ed6; - uint32 cliffDuration = 1 weeks; + uint32 cliffDuration = 500; + // uint32 cliffDuration = 1 weeks; IAccessControlManager manager = IAccessControlManager(0x0E632a15EbCBa463151B5367B4fCF91313e389a6); address distributionCreator = 0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd; + */ + + // ARBITRUM TEST + // aglaMerkl + address underlying = 0xE0688A2FE90d0f93F17f273235031062a210d691; + uint32 cliffDuration = 500; + // uint32 cliffDuration = 1 weeks; + IAccessControlManager manager = IAccessControlManager(0xA86CC1ae2D94C6ED2aB3bF68fB128c2825673267); + address distributionCreator = 0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd; // Deploy implementation PufferPointTokenWrapper implementation = new PufferPointTokenWrapper(); @@ -35,5 +47,6 @@ contract DeployPufferPointTokenWrapper is BaseScript { // Initialize PufferPointTokenWrapper(address(proxy)).initialize(underlying, cliffDuration, manager, distributionCreator); + vm.stopBroadcast(); } } From 8f604d4fdf9bc52b663145e2894280e8cd1fbf79 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Wed, 15 Jan 2025 09:10:36 +0100 Subject: [PATCH 11/13] fix: deployment --- scripts/DistributionCreator.s.sol | 8 ++++++-- scripts/deployPufferPointTokenWrapper.s.sol | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/DistributionCreator.s.sol b/scripts/DistributionCreator.s.sol index 55100a3..786b41a 100644 --- a/scripts/DistributionCreator.s.sol +++ b/scripts/DistributionCreator.s.sol @@ -192,10 +192,14 @@ contract SetUserFeeRebate is DistributionCreatorScript { // SetRewardTokenMinAmounts script contract SetRewardTokenMinAmounts is DistributionCreatorScript { + // forge script scripts/DistributionCreator.s.sol:SetRewardTokenMinAmounts --rpc-url arbitrum --sender 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701 --broadcast -i 1 function run() external { + console.log("DEPLOYER_ADDRESS:", broadcaster); // MODIFY THESE VALUES TO SET YOUR DESIRED TOKENS AND AMOUNTS - address[] memory tokens = new address[](0); - uint256[] memory amounts = new uint256[](0); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + tokens[0] = 0x0f11c6143d4Ab0d9860e984ab2dCA516E1BeCd03; + amounts[0] = 1 ether; _run(tokens, amounts); } diff --git a/scripts/deployPufferPointTokenWrapper.s.sol b/scripts/deployPufferPointTokenWrapper.s.sol index e079903..49b7757 100644 --- a/scripts/deployPufferPointTokenWrapper.s.sol +++ b/scripts/deployPufferPointTokenWrapper.s.sol @@ -5,7 +5,6 @@ import { console } from "forge-std/console.sol"; import { BaseScript } from "./utils/Base.s.sol"; -import { console } from "forge-std/console.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { ITransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -21,21 +20,22 @@ contract DeployPufferPointTokenWrapper is BaseScript { function run() public { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); - /* + address underlying = 0x282A69142bac47855C3fbE1693FcC4bA3B4d5Ed6; uint32 cliffDuration = 500; // uint32 cliffDuration = 1 weeks; IAccessControlManager manager = IAccessControlManager(0x0E632a15EbCBa463151B5367B4fCF91313e389a6); address distributionCreator = 0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd; - */ // ARBITRUM TEST + /* // aglaMerkl address underlying = 0xE0688A2FE90d0f93F17f273235031062a210d691; uint32 cliffDuration = 500; // uint32 cliffDuration = 1 weeks; IAccessControlManager manager = IAccessControlManager(0xA86CC1ae2D94C6ED2aB3bF68fB128c2825673267); address distributionCreator = 0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd; + */ // Deploy implementation PufferPointTokenWrapper implementation = new PufferPointTokenWrapper(); From edc28305708af0d96b3d8704666f244df213972d Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Fri, 17 Jan 2025 18:50:04 +0100 Subject: [PATCH 12/13] fix: updating script --- scripts/DistributionCreator.s.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/DistributionCreator.s.sol b/scripts/DistributionCreator.s.sol index 786b41a..69d3259 100644 --- a/scripts/DistributionCreator.s.sol +++ b/scripts/DistributionCreator.s.sol @@ -192,14 +192,14 @@ contract SetUserFeeRebate is DistributionCreatorScript { // SetRewardTokenMinAmounts script contract SetRewardTokenMinAmounts is DistributionCreatorScript { - // forge script scripts/DistributionCreator.s.sol:SetRewardTokenMinAmounts --rpc-url arbitrum --sender 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701 --broadcast -i 1 + // forge script scripts/DistributionCreator.s.sol:SetRewardTokenMinAmounts --rpc-url bsc --sender 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701 --broadcast -i 1 function run() external { console.log("DEPLOYER_ADDRESS:", broadcaster); // MODIFY THESE VALUES TO SET YOUR DESIRED TOKENS AND AMOUNTS address[] memory tokens = new address[](1); uint256[] memory amounts = new uint256[](1); - tokens[0] = 0x0f11c6143d4Ab0d9860e984ab2dCA516E1BeCd03; - amounts[0] = 1 ether; + tokens[0] = 0x56fA5F7BF457454Be33D8B978C86A5f5B9DD84C2; + amounts[0] = 3 * 10 ** 17; _run(tokens, amounts); } From ffd8f0373b4bf884862466799b97b97c406e8eb9 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Tue, 21 Jan 2025 19:09:53 +0100 Subject: [PATCH 13/13] fix: wrapper --- contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol index de9aac5..25edfaf 100644 --- a/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol +++ b/contracts/partners/tokenWrappers/PufferPointTokenWrapper.sol @@ -8,8 +8,8 @@ import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/exte import { IAccessControlManager } from "./BaseTokenWrapper.sol"; -import "../../utils/UUPSHelper.sol"; -import "../../utils/Errors.sol"; +import { UUPSHelper } from "../../utils/UUPSHelper.sol"; +import { Errors } from "../../utils/Errors.sol"; struct VestingID { uint128 amount; @@ -183,7 +183,6 @@ contract PufferPointTokenWrapper is UUPSHelper, ERC20Upgradeable { _; } - /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(accessControlManager) {} /// @notice Recovers any ERC20 token