├── .DS_Store ├── .env.example ├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── audits ├── Cash Audit Report.pdf ├── Cash contracts Pre-order Update - Zellic Audit Report Draft.pdf ├── NM_0289_EtherFi_Cash_Contracts_Report.pdf └── NM_0381_EtherFi_Cash_TopUps_FINAL.pdf ├── deployments ├── 1 │ └── top-ups.json ├── 8453 │ └── top-ups.json ├── 534352 │ ├── deployments.json │ ├── safe.json │ └── top-ups.json └── fixtures │ ├── fixture.json │ └── top-up-fixtures.json ├── foundry.toml ├── lcov.info ├── output ├── AddFundsToTopUpDest.json ├── MigrateKeys-1.json ├── MigrateKeys-8453.json ├── MigrateKeysGrant-534352.json ├── MigrateKeysRevoke-534352.json ├── SetUsdcConfig.json ├── SupplyUsdcToDebtManager.json ├── UpgradeSpendingLimitDelay.json └── UpgradeV2.01.json ├── package.json ├── remappings.txt ├── script ├── Gnosis │ ├── AddFundsToTopUpContract.s.sol │ ├── MigrateKeysScroll.s.sol │ ├── MigrateKeysTopUpSource.s.sol │ ├── MigrateMvp.s.sol │ ├── SupplyUsdcToDebtManager.s.sol │ ├── UpgradeTopUpDest.s.sol │ ├── UpgradeTopUpSource.s.sol │ └── UpgradeUserSafeSpendingLimitDelay.s.sol ├── MigrateMvp.s.sol ├── Recover.s.sol ├── preorder │ ├── DeployPreOrder.s.sol │ ├── DeployPreOrderImpl.s.sol │ └── UpdatePreOrder.s.sol ├── settlement-dispatcher │ ├── SettlementDispatcher.s.sol │ └── UpgradeSettlementDispatcher.s.sol ├── top-up │ ├── TopUpDest.s.sol │ ├── TopUpSource.s.sol │ ├── TopUpSourceSetConfig.s.sol │ └── bridge-adapters │ │ ├── OFTBridgeAdapter.s.sol │ │ └── StargateAdapter.s.sol └── user-safe │ ├── DeployUserSafe.s.sol │ ├── DeployUserSafeFactory.s.sol │ ├── Setup.s.sol │ ├── UpgradeDebtManager.s.sol │ ├── UpgradeUserSafeCore.s.sol │ ├── UpgradeUserSafeSetters.s.sol │ └── Utils.sol ├── src ├── UUPSProxy.sol ├── adapters │ └── aave-v3 │ │ └── EtherFiCashAaveV3Adapter.sol ├── cash-wrapper-token │ ├── CashTokenWrapperFactory.sol │ └── CashWrappedERC20.sol ├── cashback-dispatcher │ └── CashbackDispatcher.sol ├── debt-manager │ ├── DebtManagerAdmin.sol │ ├── DebtManagerCore.sol │ ├── DebtManagerInitializer.sol │ └── DebtManagerStorage.sol ├── interfaces │ ├── IAggregatorV3.sol │ ├── ICashDataProvider.sol │ ├── ICrossChainMessenger.sol │ ├── IERC20.sol │ ├── IEtherFiCashAaveV3Adapter.sol │ ├── IL2DebtManager.sol │ ├── IOFT.sol │ ├── IOneInch.sol │ ├── IOpenOcean.sol │ ├── IPriceProvider.sol │ ├── IStargate.sol │ ├── ISwapper.sol │ ├── IUserRegistry.sol │ ├── IUserSafe.sol │ ├── IWETH.sol │ └── IWeETH.sol ├── libraries │ ├── AaveLib.sol │ ├── ArrayDeDupTransientLib.sol │ ├── Base64Url.sol │ ├── EIP1271SignatureUtils.sol │ ├── OwnerLib.sol │ ├── SignatureUtils.sol │ ├── SpendingLimitLib.sol │ ├── TimeLib.sol │ ├── UserSafeLib.sol │ └── WebAuthn.sol ├── mocks │ ├── MockAaveAdapter.sol │ ├── MockERC20.sol │ ├── MockPriceProvider.sol │ ├── MockSwapper.sol │ └── UserSafeV2Mock.sol ├── oracle │ └── PriceProvider.sol ├── preorder │ ├── PreOrder.sol │ ├── custom1155.sol │ └── interfaces │ │ └── IL2DebtManager.sol ├── settlement-dispatcher │ └── SettlementDispatcher.sol ├── top-up │ ├── TopUpDest.sol │ ├── TopUpSource.sol │ └── bridges │ │ ├── BridgeAdapterBase.sol │ │ ├── EtherFiOFTBridgeAdapter.sol │ │ └── StargateAdapter.sol ├── user-safe │ ├── UserSafeCore.sol │ ├── UserSafeEventEmitter.sol │ ├── UserSafeFactory.sol │ ├── UserSafeLens.sol │ ├── UserSafeSetters.sol │ └── UserSafeStorage.sol └── utils │ ├── CashDataProvider.sol │ ├── ReentrancyGuardTransientUpgradeable.sol │ ├── StorageSlot.sol │ ├── Swapper1InchV6.sol │ └── SwapperOpenOcean.sol ├── test ├── .DS_Store ├── AaveAdapter │ └── AaveAdapter.t.sol ├── AaveUnitTest.t.sol ├── CashDataProvider │ └── CashDataProvider.t.sol ├── CashWrappedToken │ ├── CashWrappedToken.t.sol │ └── CashWrappedTokenFactory.t.sol ├── CashbackDispatcher │ └── CashbackDispatcher.t.sol ├── DebtManager │ ├── .DS_Store │ ├── Borrow.t.sol │ ├── Collateral.t.sol │ ├── Deploy.t.sol │ ├── Liquidate.t.sol │ ├── Repay.t.sol │ └── SupplyAndWithdraw.t.sol ├── ForkTest.t.sol ├── Gnosis │ ├── MigrateMvp.t.sol │ └── SetUsdcConfig.t.sol ├── IntegrationTest │ ├── .DS_Store │ └── IntegrationTest.t.sol ├── PreOrder.t.sol ├── PriceProvider │ └── PriceProvider.t.sol ├── SettlementDispatcher │ └── SettlementDispatcher.t.sol ├── Setup.t.sol ├── Swapper │ ├── Swapper1Inch.t.sol │ └── SwapperOpenOcean.t.sol ├── TimeLib.t.sol ├── TopUp │ ├── TopUpDest.t.sol │ └── TopUpSource.t.sol ├── UserSafe │ ├── .DS_Store │ ├── CanSpend.t.sol │ ├── Deploy.t.sol │ ├── Mode.t.sol │ ├── Owner.t.sol │ ├── PendingCashback.t.sol │ ├── Recovery.t.sol │ ├── RecoveryUsingSafe.t.sol │ ├── Spend.t.sol │ ├── SpendingLimit.t.sol │ ├── Swap.t.sol │ ├── UserSafeFactory.t.sol │ ├── UserSafeLens.t.sol │ ├── WebAuthn.t.sol │ └── Withdrawal.t.sol ├── Utils.sol ├── WebAuthnUtils.sol ├── getQuote1Inch.ts ├── getQuoteOpenOcean.ts └── proposeRecoverySignature.ts ├── utils └── GnosisHelpers.sol └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TEST_CHAIN="local" # chainid, put local in case it is not fork test 2 | PRIVATE_KEY="" 3 | 4 | SCROLL_RPC="https://1rpc.io/scroll" 5 | SCROLLSCAN_KEY="" 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | /broadcast/* 7 | !/broadcast 8 | /broadcast/*/31337/ 9 | /broadcast/**/dry-run/ 10 | 11 | # Docs 12 | docs/ 13 | 14 | # Coverage 15 | report 16 | 17 | # Dotenv file 18 | .env 19 | 20 | # Certora 21 | .certora_internal 22 | 23 | # Coverage 24 | report 25 | lcov.info 26 | 27 | # vscode 28 | .vscode/ 29 | 30 | node_modules -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts-upgradeable"] 5 | path = lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "lib/solmate"] 11 | path = lib/solmate 12 | url = https://github.com/transmissions11/solmate 13 | [submodule "lib/aave-v3-core"] 14 | path = lib/aave-v3-core 15 | url = https://github.com/aave/aave-v3-core 16 | [submodule "lib/solady"] 17 | path = lib/solady 18 | url = https://github.com/Vectorized/solady 19 | [submodule "lib/crypto-lib"] 20 | path = lib/crypto-lib 21 | url = https://github.com/get-smooth/crypto-lib 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ether.fi Cash Smart Contracts 2 | 3 | Welcome to the ether.fi Cash Smart Contracts repository! This project powers the ether.fi Cash product, providing seamless debit and credit functionalities for users. 4 | 5 | ## Overview 6 | 7 | ether.fi Cash allows users to manage their funds through two primary mechanisms: 8 | 9 | - **Debit:** Users spend their own funds via the ether.fi card, with transactions flowing directly from their UserSafe contracts to the Settlement Dispatcher contract. 10 | - **Credit:** Users can borrow funds from the ether.fi Debt Manager by holding collateral with their UserSafe. These funds are available for spending with the ether.fi card, much like a traditional credit card, but backed by the user's collateral. 11 | 12 | ## Key Contracts 13 | 14 | The project comprises several smart contracts that ensure secure and efficient handling of user funds, collateral, and borrowing. Some of the main components include: 15 | 16 | - **UserSafe**: Manages user-owned assets and permissions. 17 | - **L2DebtManager**: Handles debt management for credit flows. 18 | - **PriceProvider**: Supplies price data for collateral valuation. 19 | 20 | ## Get Started 21 | 22 | To deploy and interact with these smart contracts, clone the repository and follow the build and test instructions provided below. 23 | 24 | ### Clone the repository 25 | 26 | ```shell 27 | git clone https://github.com/etherfi-protocol/cash-contracts 28 | ``` 29 | 30 | ### Install dependencies 31 | 32 | ```shell 33 | yarn 34 | ``` 35 | 36 | ### Build the repo 37 | 38 | ```shell 39 | yarn build 40 | ``` 41 | 42 | ### Test 43 | 44 | ```shell 45 | yarn test 46 | ``` 47 | 48 | ## Security 49 | 50 | The contracts are designed with security in mind, incorporating features like spending limits, delayed withdrawals, and recovery mechanisms. 51 | -------------------------------------------------------------------------------- /audits/Cash Audit Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/audits/Cash Audit Report.pdf -------------------------------------------------------------------------------- /audits/Cash contracts Pre-order Update - Zellic Audit Report Draft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/audits/Cash contracts Pre-order Update - Zellic Audit Report Draft.pdf -------------------------------------------------------------------------------- /audits/NM_0289_EtherFi_Cash_Contracts_Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/audits/NM_0289_EtherFi_Cash_Contracts_Report.pdf -------------------------------------------------------------------------------- /audits/NM_0381_EtherFi_Cash_TopUps_FINAL.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/audits/NM_0381_EtherFi_Cash_TopUps_FINAL.pdf -------------------------------------------------------------------------------- /deployments/1/top-ups.json: -------------------------------------------------------------------------------- 1 | { 2 | "topUps": { 3 | "topUpSourceProxy": "0xC85276fec421d0CA3c0eFd4be2B7F569bc7b5b99", 4 | "topUpSourceImpl": "0xa2F1d5eBFD11299ff12F2B5F177C158e9Fa1E1C3" 5 | }, 6 | "stargateAdapter": "0x1c83858e006d8d1bfba09341eb0754181b23c01d", 7 | "etherFiOFTBridgeAdapter": "0xc4eB9bc777de10d0f237b18be7A05a125C4414c9" 8 | } -------------------------------------------------------------------------------- /deployments/534352/deployments.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "cashDataProviderImpl": "0xDFB1c521423Ff2bb1Eb6AFCDDA545F20545F5796", 4 | "cashDataProviderProxy": "0xb1F5bBc3e4DE0c767ace41EAb8A28b837fBA966F", 5 | "cashbackDispatcherImpl": "0x16745eFB1a8efdAfC6622f534cCBb7574AFD55E7", 6 | "cashbackDispatcherProxy": "0x7d372C3ca903CA2B6ecd8600D567eb6bAfC5e6c9", 7 | "debtManagerAdminImpl": "0x325173ED24F7cB715f7Fc6C6A0be10d5330772A7", 8 | "debtManagerCore": "0xeA25223B4677B4c3D41F3AE9c831c501bE780b0E", 9 | "debtManagerInitializer": "0x318c5F25E44ae661b06DCB9278648475Ee52F814", 10 | "debtManagerProxy": "0x8f9d2Cd33551CE06dD0564Ba147513F715c2F4a0", 11 | "etherFiWallet": "0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00", 12 | "owner": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4", 13 | "priceProvider": "0x8B4C8c403fc015C46061A8702799490FD616E3bf", 14 | "recoverySigner1": "0xbED1b10aF02D48DA7dA0Fff26d16E0873AF46706", 15 | "recoverySigner2": "0x566E58ac0F2c4BCaF6De63760C56cC3f825C48f5", 16 | "settlementDispatcherImpl": "0x5A15a6198878c04c9d887BAE01Cfdba5456AD820", 17 | "settlementDispatcherProxy": "0x4Dca5093E0bB450D7f7961b5Df0A9d4c24B24786", 18 | "swapper": "0x44C00821F0e70F00b7af74235981eb30BEB3577F", 19 | "usdc": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", 20 | "userSafeCoreImpl": "0x11529B4776EC1940277A5F1fF2fB1Cc7100B31b0", 21 | "userSafeEventEmitterImpl": "0x5a85Fc47F678f715ec72b098EB8570e113CBa5D5", 22 | "userSafeEventEmitterProxy": "0x5423885B376eBb4e6104b8Ab1A908D350F6A162e", 23 | "userSafeFactoryImpl": "0x8273c252C51E692dC3903073AC568620B5618F90", 24 | "userSafeFactoryProxy": "0x18Fa07dF94b4E9F09844e1128483801B24Fe8a27", 25 | "userSafeLensImpl": "0x1073EF7f8245dDF3537aacdA2f2DE6D44BeD0Abf", 26 | "userSafeLensProxy": "0x333321a783f765bFd4c22FBBC5B2D02b97efB44c", 27 | "userSafeSettersImpl": "0x712AA225d41FD3C39De5eC6449237DfB8a7fF298", 28 | "weETH": "0x01f0a31698C4d065659b9bdC21B3610292a1c506" 29 | } 30 | } -------------------------------------------------------------------------------- /deployments/534352/safe.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /deployments/534352/top-ups.json: -------------------------------------------------------------------------------- 1 | { 2 | "topUps": { 3 | "topUpDestProxy": "0xeb61c16A60ab1b4a9a1F8E92305808F949F4Ea9B", 4 | "topUpDestImpl": "0xEE880b59075cF486d88B46C33c5c2f95B08f2582" 5 | } 6 | } -------------------------------------------------------------------------------- /deployments/8453/top-ups.json: -------------------------------------------------------------------------------- 1 | { 2 | "topUps": { 3 | "topUpSourceProxy": "0xC85276fec421d0CA3c0eFd4be2B7F569bc7b5b99", 4 | "topUpSourceImpl": "0xa2F1d5eBFD11299ff12F2B5F177C158e9Fa1E1C3" 5 | }, 6 | "stargateAdapter": "0x1C83858e006D8D1bfBa09341eB0754181b23c01d", 7 | "etherFiOFTBridgeAdapter": "0xc4eB9bc777de10d0f237b18be7A05a125C4414c9" 8 | } -------------------------------------------------------------------------------- /deployments/fixtures/fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "534352": { 3 | "rpc": "https://rpc.scroll.io", 4 | "weth": "0x5300000000000000000000000000000000000004", 5 | "usdc": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", 6 | "usdt": "0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df", 7 | "weETH": "0x01f0a31698C4d065659b9bdC21B3610292a1c506", 8 | "scr": "0xd29687c813d741e2f938f4ac377128810e217b1b", 9 | "weEthWethOracle": "0x57bd9E614f542fB3d6FeF2B744f3B813f0cc1258", 10 | "ethUsdcOracle": "0x6bF14CB0A831078629D993FDeBcB182b21A8774C", 11 | "scrUsdOracle": "0x26f6F7C468EE309115d19Aa2055db5A74F8cE7A5", 12 | "usdcUsdOracle": "0x43d12Fb3AfCAd5347fA764EeAB105478337b7200", 13 | "swapRouter1InchV6": "0x111111125421cA6dc452d289314280a0f8842A65", 14 | "swapRouterOpenOcean": "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64", 15 | "aaveV3Pool": "0x11fCfe756c05AD438e312a7fd934381537D3cFfe", 16 | "aaveV3PoolDataProvider": "0xa99F4E69acF23C6838DE90dD1B5c02EA928A53ee", 17 | "stargateUsdcPool": "0x3Fc69CC4A842838bCDC9499178740226062b14E4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /deployments/fixtures/top-up-fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "534352": { 3 | "owner": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4" 4 | }, 5 | "1": { 6 | "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 7 | "owner": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4", 8 | "recoveryWallet": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4", 9 | "tokenConfigs":[ 10 | { 11 | "token": "usdc", 12 | "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 13 | "bridge": "stargate", 14 | "maxSlippageInBps": "50", 15 | "stargatePool": "0xc026395860Db2d07ee33e05fE50ed7bD583189C7" 16 | }, 17 | { 18 | "token": "weETH", 19 | "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", 20 | "bridge": "oftBridgeAdapter", 21 | "maxSlippageInBps": "50", 22 | "oftAdapter": "0xcd2eb13D6831d4602D80E5db9230A57596CDCA63" 23 | } 24 | ] 25 | }, 26 | "8453": { 27 | "weth": "0x4200000000000000000000000000000000000006", 28 | "owner": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4", 29 | "recoveryWallet": "0xA6cf33124cb342D1c604cAC87986B965F428AAC4", 30 | "tokenConfigs":[ 31 | { 32 | "token": "usdc", 33 | "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 34 | "bridge": "stargate", 35 | "maxSlippageInBps": "50", 36 | "stargatePool": "0x27a16dc786820B16E5c9028b75B99F6f604b5d26" 37 | }, 38 | { 39 | "token": "weETH", 40 | "address": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A", 41 | "bridge": "oftBridgeAdapter", 42 | "maxSlippageInBps": "50", 43 | "oftAdapter": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A" 44 | } 45 | ] 46 | } 47 | } -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.24" 6 | cbor_metadata = true 7 | ffi = true 8 | optimizer = true 9 | optimizer_runs = 100 10 | evm_version = "cancun" 11 | ast = true 12 | build_info = true 13 | extra_output = ["storageLayout"] 14 | gas_reports = ["*"] 15 | fs_permissions = [{ access = "read-write", path = "./" }] 16 | 17 | [rpc_endpoints] 18 | mainnet = "${MAINNET_RPC}" 19 | sepolia = "${SEPOLIA_RPC}" 20 | scroll = "${SCROLL_RPC}" 21 | base = "${BASE_RPC}" 22 | fi_sepolia = "${FI_SEPOLIA_RPC}" 23 | arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC}" 24 | 25 | [etherscan] 26 | mainnet = { key = "${MAINNET_ETHERSCAN_KEY}" } 27 | sepolia = { key = "${MAINNET_ETHERSCAN_KEY}" } 28 | scroll = { key = "${SCROLLSCAN_KEY}" } 29 | base = { key = "${BASESCAN_KEY}" } 30 | fi_sepolia = { url = "https://fi-sepolia-explorer.ether.fi/api", key = "5P1ZGHEFGJUN2N1T9NY6NVVDXQDJKPMD2Z" } 31 | arbitrum_sepolia = { key = "${ARBISCAN_KEY}" } 32 | -------------------------------------------------------------------------------- /output/AddFundsToTopUpDest.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4", 7 | "value": "0", 8 | "data": "0x095ea7b3000000000000000000000000eb61c16a60ab1b4a9a1f8e92305808f949f4ea9b000000000000000000000000000000000000000000000000000000174876e800" 9 | }, 10 | { 11 | "to": "0xeb61c16a60ab1b4a9a1f8e92305808f949f4ea9b", 12 | "value": "0", 13 | "data": "0x47e7ef2400000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a4000000000000000000000000000000000000000000000000000000174876e800" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /output/MigrateKeys-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "1", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0xc85276fec421d0ca3c0efd4be2b7f569bc7b5b99", 7 | "value": "0", 8 | "data": "0xd547741fc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a80000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 9 | }, 10 | { 11 | "to": "0xc85276fec421d0ca3c0efd4be2b7f569bc7b5b99", 12 | "value": "0", 13 | "data": "0x2f2ff15dc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a8000000000000000000000000b473201cbfc2ed6fed9ed960faccd9e733b1c26e" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /output/MigrateKeys-8453.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "8453", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0xc85276fec421d0ca3c0efd4be2b7f569bc7b5b99", 7 | "value": "0", 8 | "data": "0xd547741fc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a80000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 9 | }, 10 | { 11 | "to": "0xc85276fec421d0ca3c0efd4be2b7f569bc7b5b99", 12 | "value": "0", 13 | "data": "0x2f2ff15dc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a8000000000000000000000000b473201cbfc2ed6fed9ed960faccd9e733b1c26e" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /output/MigrateKeysGrant-534352.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 7 | "value": "0", 8 | "data": "0x82947e2e000000000000000000000000334962e5a3997ee1d480eecc64f4809db308b6f6" 9 | }, 10 | { 11 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 12 | "value": "0", 13 | "data": "0x82947e2e000000000000000000000000c8e5a04b85513cdb99ac6301e35f80f039d5dcc5" 14 | }, 15 | { 16 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 17 | "value": "0", 18 | "data": "0x2f2ff15da49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775000000000000000000000000c8e5a04b85513cdb99ac6301e35f80f039d5dcc5" 19 | }, 20 | { 21 | "to": "0xeb61c16a60ab1b4a9a1f8e92305808f949f4ea9b", 22 | "value": "0", 23 | "data": "0x2f2ff15d5e4bd437d29fad01c10cdcfff414f0d6b0e84b96d2dade88d780d45b5630696b000000000000000000000000c6f5a0182ad9f92db99562e98e252afbeaaca582" 24 | }, 25 | { 26 | "to": "0xeb61c16a60ab1b4a9a1f8e92305808f949f4ea9b", 27 | "value": "0", 28 | "data": "0x2f2ff15d5e4bd437d29fad01c10cdcfff414f0d6b0e84b96d2dade88d780d45b5630696b000000000000000000000000b473201cbfc2ed6fed9ed960faccd9e733b1c26e" 29 | }, 30 | { 31 | "to": "0x4dca5093e0bb450d7f7961b5df0a9d4c24b24786", 32 | "value": "0", 33 | "data": "0x2f2ff15dc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a8000000000000000000000000b473201cbfc2ed6fed9ed960faccd9e733b1c26e" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /output/MigrateKeysRevoke-534352.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 7 | "value": "0", 8 | "data": "0xeec4ca7e0000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 9 | }, 10 | { 11 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 12 | "value": "0", 13 | "data": "0xd547741fa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c217750000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 14 | }, 15 | { 16 | "to": "0xeb61c16a60ab1b4a9a1f8e92305808f949f4ea9b", 17 | "value": "0", 18 | "data": "0xd547741f5e4bd437d29fad01c10cdcfff414f0d6b0e84b96d2dade88d780d45b5630696b0000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 19 | }, 20 | { 21 | "to": "0x4dca5093e0bb450d7f7961b5df0a9d4c24b24786", 22 | "value": "0", 23 | "data": "0xd547741fc809a7fd521f10cdc3c068621a1c61d5fd9bb3f1502a773e53811bc248d919a80000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /output/SetUsdcConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0x8f9d2cd33551ce06dd0564ba147513f715c2f4a0", 7 | "value": "0", 8 | "data": "0xb7fe2f2800000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a4000000000000000000000000000000000000000000000004e1003b28d92800000000000000000000000000000000000000000000000000052663ccab1e1c00000000000000000000000000000000000000000000000000000de0b6b3a7640000" 9 | }, 10 | { 11 | "to": "0x8f9d2cd33551ce06dd0564ba147513f715c2f4a0", 12 | "value": "0", 13 | "data": "0x3b155e4100000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a40000000000000000000000000000000000000000000000000000000000000001" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /output/SupplyUsdcToDebtManager.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4", 7 | "value": "0", 8 | "data": "0x095ea7b30000000000000000000000008f9d2cd33551ce06dd0564ba147513f715c2f4a0000000000000000000000000000000000000000000000000000000174876e800" 9 | }, 10 | { 11 | "to": "0x8f9d2cd33551ce06dd0564ba147513f715c2f4a0", 12 | "value": "0", 13 | "data": "0x0c0a769b000000000000000000000000261bec28b8a3bb5098436c9a918bed1270fff1e400000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a4000000000000000000000000000000000000000000000000000000174876e800" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /output/UpgradeSpendingLimitDelay.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 7 | "value": "0", 8 | "data": "0x9a5282b8000000000000000000000000bf78560265d4cd6d9dddb2faf68de97c7d8ccb8e" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /output/UpgradeV2.01.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "534352", 3 | "meta": { "txBuilderVersion": "1.16.5" }, 4 | "transactions": [ 5 | { 6 | "to": "0x4dca5093e0bb450d7f7961b5df0a9d4c24b24786", 7 | "value": "0", 8 | "data": "0x5703e43400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a40000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000759f0000000000000000000000006f7f522075aa5483d049df0ef81fcdd3b0ace7f40000000000000000000000003fc69cc4a842838bcdc9499178740226062b14e4" 9 | }, 10 | { 11 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 12 | "value": "0", 13 | "data": "0x4f1ef286000000000000000000000000d4299cb1a8d1488df47a544bae309b00b8cbf66f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000" 14 | }, 15 | { 16 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 17 | "value": "0", 18 | "data": "0x0bbcd36800000000000000000000000039e2e944965c2848f30ddef694f37fa72a6e1c55" 19 | }, 20 | { 21 | "to": "0x18fa07df94b4e9f09844e1128483801b24fe8a27", 22 | "value": "0", 23 | "data": "0x9a5282b8000000000000000000000000b179b2b0dcf848970aea5b95c23c50e6c0ad37dd" 24 | }, 25 | { 26 | "to": "0x8f9d2cd33551ce06dd0564ba147513f715c2f4a0", 27 | "value": "0", 28 | "data": "0x4f1ef2860000000000000000000000001b887355a3c50d570caa0d324991dd66c22a6a1c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000" 29 | }, 30 | { 31 | "to": "0x8f9d2cd33551ce06dd0564ba147513f715c2f4a0", 32 | "value": "0", 33 | "data": "0xfc0cfeee0000000000000000000000005fd544b6b45d6ce5e72a6315adab8ccbc041f06f" 34 | }, 35 | { 36 | "to": "0x7d372c3ca903ca2b6ecd8600d567eb6bafc5e6c9", 37 | "value": "0", 38 | "data": "0x4f1ef286000000000000000000000000afb5929400fd6922c5dd3d1ec7fa955cb331148900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000" 39 | }, 40 | { 41 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 42 | "value": "0", 43 | "data": "0x4f1ef28600000000000000000000000029e76c77f2aa51bf67f6badfa6437d6908dea0e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000" 44 | }, 45 | { 46 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 47 | "value": "0", 48 | "data": "0x1f7f6e21000000000000000000000000b3545ff45431a3dccfeaa37e4f51d663a5d2362c" 49 | }, 50 | { 51 | "to": "0xb1f5bbc3e4de0c767ace41eab8a28b837fba966f", 52 | "value": "0", 53 | "data": "0xd547741fa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c217750000000000000000000000002e0be8d3d9f1833fbacf9a5e9f2d470817ff0c00" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ether-fi-cash-contracts", 3 | "description": "This project powers the ether.fi Cash product, providing seamless debit and credit functionalities for users.", 4 | "version": "v0.5", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Shivam Agrawal", 8 | "email": "shivam@ether.fi" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Seongyun Ko", 13 | "email": "seongyun@ether.fi" 14 | } 15 | ], 16 | "scripts": { 17 | "build": "forge build", 18 | "test": "forge test", 19 | "compile": "forge compile" 20 | }, 21 | "dependencies": { 22 | "@safe-global/api-kit": "^2.5.7", 23 | "@safe-global/protocol-kit": "^5.2.0", 24 | "@safe-global/types-kit": "^1.0.1", 25 | "axios": "^1.7.8", 26 | "dotenv": "^16.4.7", 27 | "ethers": "^5.7.2" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^22.1.0" 31 | }, 32 | "keywords": [ 33 | "ether.fi", 34 | "cash-contracts", 35 | "ethfi", 36 | "card", 37 | "crypto" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 3 | @aave/=lib/aave-v3-core/contracts/ 4 | SmoothCryptoLib=lib/crypto-lib/src -------------------------------------------------------------------------------- /script/Gnosis/AddFundsToTopUpContract.s.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: UNLICENSED 3 | pragma solidity ^0.8.24; 4 | 5 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 6 | import {Utils} from "../user-safe/Utils.sol"; 7 | 8 | contract AddFundsToTopUpContract is GnosisHelpers, Utils { 9 | address fundsSender = 0x261bEC28B8a3BB5098436c9a918bED1270FFF1E4; 10 | address usdc = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; 11 | uint256 amount = 100_000e6; 12 | address topUpDest = 0xeb61c16A60ab1b4a9a1F8E92305808F949F4Ea9B; 13 | 14 | function run() public { 15 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 16 | 17 | string memory approval = iToHex(abi.encodeWithSignature("approve(address,uint256)", topUpDest, amount)); 18 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(usdc), approval, false))); 19 | 20 | string memory addFunds = iToHex(abi.encodeWithSignature("deposit(address,uint256)", usdc, amount)); 21 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpDest), addFunds, true))); 22 | 23 | vm.createDir("./output", true); 24 | string memory path = "./output/AddFundsToTopUpDest.json"; 25 | 26 | vm.writeFile(path, gnosisTx); 27 | 28 | executeGnosisTransactionBundle(path, fundsSender); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /script/Gnosis/MigrateKeysScroll.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 5 | 6 | contract MigrateKeysScroll is GnosisHelpers { 7 | bytes32 ADMIN_ROLE = keccak256("ADMIN_ROLE"); 8 | bytes32 TOP_UP_ROLE = keccak256("TOP_UP_ROLE"); 9 | bytes32 BRIDGER_ROLE = keccak256("BRIDGER_ROLE"); 10 | 11 | address owner = 0xA6cf33124cb342D1c604cAC87986B965F428AAC4; 12 | address cashDataProvider = 0xb1F5bBc3e4DE0c767ace41EAb8A28b837fBA966F; 13 | address userSafeFactory = 0x18Fa07dF94b4E9F09844e1128483801B24Fe8a27; 14 | address topUpDestScroll = 0xeb61c16A60ab1b4a9a1F8E92305808F949F4Ea9B; 15 | address settlementDispatcher = 0x4Dca5093E0bB450D7f7961b5Df0A9d4c24B24786; 16 | 17 | address currentEtherFiWallet = 0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00; 18 | address currentUserSafeFactoryAdmin = 0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00; 19 | address currentTopUpAdmin = 0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00; 20 | address currentDispatcherBridger = 0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00; 21 | 22 | address newUserSafeFactoryAdmin = 0xc8E5a04b85513cDb99ac6301E35f80F039D5dCC5; 23 | address newTopUpAdminCashBE = 0xc6F5A0182aD9F92dB99562e98e252aFbEaaca582; 24 | address newEtherFiWallet = 0x334962E5a3997eE1D480EECc64F4809dB308b6f6; 25 | address newTopUpAdminTopUpBE = 0xb473201cbFc2ed6FEd9eD960fACCD9E733B1C26E; 26 | address newDispatcherBridger = 0xb473201cbFc2ed6FEd9eD960fACCD9E733B1C26E; 27 | 28 | function run() public { 29 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 30 | 31 | // revoke roles scroll 32 | string memory revokeEtherFiWalletRole = iToHex(abi.encodeWithSignature("revokeEtherFiWalletRole(address)", currentEtherFiWallet)); 33 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(cashDataProvider), revokeEtherFiWalletRole, false))); 34 | 35 | string memory revokeAdminRoleOnFactory = iToHex(abi.encodeWithSignature("revokeRole(bytes32,address)", ADMIN_ROLE, currentUserSafeFactoryAdmin)); 36 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(userSafeFactory), revokeAdminRoleOnFactory, false))); 37 | 38 | string memory revokeTopUpRoleOnTopUpDest = iToHex(abi.encodeWithSignature("revokeRole(bytes32,address)", TOP_UP_ROLE, currentTopUpAdmin)); 39 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpDestScroll), revokeTopUpRoleOnTopUpDest, false))); 40 | 41 | string memory revokeBridgerRoleOnDispatcher = iToHex(abi.encodeWithSignature("revokeRole(bytes32,address)", BRIDGER_ROLE, currentDispatcherBridger)); 42 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(settlementDispatcher), revokeBridgerRoleOnDispatcher, false))); 43 | 44 | // grant roles 45 | string memory grantEtherFiWalletRole1 = iToHex(abi.encodeWithSignature("grantEtherFiWalletRole(address)", newEtherFiWallet)); 46 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(cashDataProvider), grantEtherFiWalletRole1, false))); 47 | 48 | string memory grantEtherFiWalletRole2 = iToHex(abi.encodeWithSignature("grantEtherFiWalletRole(address)", newUserSafeFactoryAdmin)); 49 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(cashDataProvider), grantEtherFiWalletRole2, false))); 50 | 51 | string memory grantAdminRoleOnFactory = iToHex(abi.encodeWithSignature("grantRole(bytes32,address)", ADMIN_ROLE, newUserSafeFactoryAdmin)); 52 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(userSafeFactory), grantAdminRoleOnFactory, false))); 53 | 54 | string memory grantTopUpRoleOnTopUpDest1 = iToHex(abi.encodeWithSignature("grantRole(bytes32,address)", TOP_UP_ROLE, newTopUpAdminCashBE)); 55 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpDestScroll), grantTopUpRoleOnTopUpDest1, false))); 56 | 57 | string memory grantTopUpRoleOnTopUpDest2 = iToHex(abi.encodeWithSignature("grantRole(bytes32,address)", TOP_UP_ROLE, newTopUpAdminTopUpBE)); 58 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpDestScroll), grantTopUpRoleOnTopUpDest2, false))); 59 | 60 | string memory grantBridgerRoleOnDispatcher = iToHex(abi.encodeWithSignature("grantRole(bytes32,address)", BRIDGER_ROLE, newDispatcherBridger)); 61 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(settlementDispatcher), grantBridgerRoleOnDispatcher, true))); 62 | 63 | vm.createDir("./output", true); 64 | string memory path = string(abi.encodePacked("./output/MigrateKeys-", vm.toString(block.chainid), ".json")); 65 | 66 | vm.writeFile(path, gnosisTx); 67 | } 68 | } -------------------------------------------------------------------------------- /script/Gnosis/MigrateKeysTopUpSource.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 5 | import {Utils} from "../user-safe/Utils.sol"; 6 | 7 | contract MigrateKeysTopUpSource is Utils, GnosisHelpers { 8 | bytes32 BRIDGER_ROLE = keccak256("BRIDGER_ROLE"); 9 | 10 | address currentTopUpBridger = 0x2e0BE8D3D9f1833fbACf9A5e9f2d470817Ff0c00; 11 | address newTopUpBridger = 0xb473201cbFc2ed6FEd9eD960fACCD9E733B1C26E; 12 | 13 | address topUpSource = 0xC85276fec421d0CA3c0eFd4be2B7F569bc7b5b99; 14 | 15 | function run() public { 16 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 17 | 18 | string memory revokeTopUpSourceBridgerRole = iToHex(abi.encodeWithSignature("revokeRole(bytes32,address)", BRIDGER_ROLE, currentTopUpBridger)); 19 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpSource), revokeTopUpSourceBridgerRole, false))); 20 | 21 | string memory grantTopUpSourceBridgerRole = iToHex(abi.encodeWithSignature("grantRole(bytes32,address)", BRIDGER_ROLE, newTopUpBridger)); 22 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpSource), grantTopUpSourceBridgerRole, true))); 23 | 24 | vm.createDir("./output", true); 25 | string memory path = string(abi.encodePacked("./output/MigrateKeys-", vm.toString(block.chainid), ".json")); 26 | 27 | vm.writeFile(path, gnosisTx); 28 | } 29 | } -------------------------------------------------------------------------------- /script/Gnosis/SupplyUsdcToDebtManager.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 5 | import {Utils} from "../user-safe/Utils.sol"; 6 | import "forge-std/Test.sol"; 7 | 8 | contract SupplyUsdcToDebtManager is GnosisHelpers, Utils, Test { 9 | address fundsSender = 0x261bEC28B8a3BB5098436c9a918bED1270FFF1E4; 10 | address usdc = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; 11 | uint256 amount = 100_000e6; 12 | address debtManager = 0x8f9d2Cd33551CE06dD0564Ba147513F715c2F4a0; 13 | 14 | function run() public { 15 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 16 | 17 | string memory approval = iToHex(abi.encodeWithSignature("approve(address,uint256)", debtManager, amount)); 18 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(usdc), approval, false))); 19 | 20 | string memory addFunds = iToHex(abi.encodeWithSignature("supply(address,address,uint256)", fundsSender, usdc, amount)); 21 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(debtManager), addFunds, true))); 22 | 23 | vm.createDir("./output", true); 24 | string memory path = "./output/SupplyUsdcToDebtManager.json"; 25 | 26 | vm.writeFile(path, gnosisTx); 27 | 28 | executeGnosisTransactionBundle(path, fundsSender); 29 | } 30 | } -------------------------------------------------------------------------------- /script/Gnosis/UpgradeTopUpDest.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 5 | import {Utils} from "../user-safe/Utils.sol"; 6 | import {TopUpDest} from "../../src/top-up/TopUpDest.sol"; 7 | 8 | contract MigrateTopUpDest is Utils, GnosisHelpers { 9 | address topUpDest = 0xeb61c16A60ab1b4a9a1F8E92305808F949F4Ea9B; 10 | 11 | function run() public { 12 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 13 | vm.startBroadcast(deployerPrivateKey); 14 | 15 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 16 | address topUpDestImpl = address(new TopUpDest()); 17 | 18 | string memory upgradeTopUpDest = iToHex(abi.encodeWithSignature("upgradeToAndCall(address,bytes)", topUpDestImpl, "")); 19 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpDest), upgradeTopUpDest, true))); 20 | 21 | vm.createDir("./output", true); 22 | string memory path = string(abi.encodePacked("./output/UpgradeTopUpDest.json")); 23 | 24 | vm.writeFile(path, gnosisTx); 25 | 26 | vm.stopBroadcast(); 27 | } 28 | } -------------------------------------------------------------------------------- /script/Gnosis/UpgradeTopUpSource.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 5 | import {Utils} from "../../script/user-safe/Utils.sol"; 6 | import {TopUpSource} from "../../src/top-up/TopUpSource.sol"; 7 | 8 | contract UpgradeTopUpSource is Utils, GnosisHelpers { 9 | address topUpSource = 0xC85276fec421d0CA3c0eFd4be2B7F569bc7b5b99; 10 | 11 | function run() public { 12 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 13 | vm.startBroadcast(deployerPrivateKey); 14 | 15 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 16 | address topUpSourceImpl = address(new TopUpSource()); 17 | 18 | string memory upgradeTopUpSource = iToHex(abi.encodeWithSignature("upgradeToAndCall(address,bytes)", topUpSourceImpl, "")); 19 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(topUpSource), upgradeTopUpSource, true))); 20 | 21 | vm.createDir("./output", true); 22 | string memory path = string(abi.encodePacked("./output/UpgradeTopUpSrc", "-", vm.toString(block.chainid), ".json")); 23 | 24 | vm.writeFile(path, gnosisTx); 25 | 26 | vm.stopBroadcast(); 27 | } 28 | } -------------------------------------------------------------------------------- /script/Gnosis/UpgradeUserSafeSpendingLimitDelay.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {UserSafeSetters} from "../../src/user-safe/UserSafeSetters.sol"; 5 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 6 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 7 | import {Utils} from "../user-safe/Utils.sol"; 8 | 9 | contract UpgradeUserSafeSpendingLimitDelay is Utils, GnosisHelpers { 10 | address userSafeFactory = 0x18Fa07dF94b4E9F09844e1128483801B24Fe8a27; 11 | address cashDataProvider = 0xb1F5bBc3e4DE0c767ace41EAb8A28b837fBA966F; 12 | 13 | function run() public { 14 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 15 | vm.startBroadcast(deployerPrivateKey); 16 | address userSafeSettersImpl = address(new UserSafeSetters(cashDataProvider)); 17 | 18 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 19 | string memory userSafeSettersUpgrade = iToHex(abi.encodeWithSignature("setUserSafeSettersImpl(address)", userSafeSettersImpl)); 20 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(userSafeFactory), userSafeSettersUpgrade, true))); 21 | 22 | vm.createDir("./output", true); 23 | string memory path = "./output/UpgradeSpendingLimitDelay.json"; 24 | 25 | vm.writeFile(path, gnosisTx); 26 | 27 | vm.stopBroadcast(); 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /script/Recover.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {console} from "forge-std/console.sol"; 7 | import {IUserSafe} from "../src/interfaces/IUserSafe.sol"; 8 | 9 | contract RecoverSafe is Script { 10 | using MessageHashUtils for bytes32; 11 | 12 | bytes32 public constant RECOVERY_METHOD = keccak256("recoverUserSafe"); 13 | 14 | IUserSafe userSafe = IUserSafe(0x0c188cD4679C7337Ef1dFC97B7af55461B62Aa3e); 15 | bytes newOwnerBytes = hex"b8f875ac054b9d83d37f340a08d04106d26a1b094171b4eebc01dbd47c6cba13e4603e8e90bb076073272b2a9be265b7dfb260ec088f7732f51957413355474d"; 16 | 17 | function run() external { 18 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 19 | 20 | vm.startBroadcast(deployerPrivateKey); 21 | 22 | IUserSafe.Signature[2] memory signatures; 23 | 24 | signatures[0] = IUserSafe.Signature({ 25 | index: 1, // index 1 for etherfi signer safe 26 | signature: "" // no need to pass a sig since executed onchain 27 | }); 28 | 29 | signatures[1] = IUserSafe.Signature({ 30 | index: 2, // index 2 for third party safe 31 | signature: hex"72ec31877fb48e67e039bbb7ef83dc221a883559750ce4fd7a1b668a41cf8e361659b0159cc8fa4b405dd76bb405f66237ae57a5e231498fea8a2a8eae46673e1b" // no need to pass a sig since executed onchain 32 | }); 33 | 34 | userSafe.recoverUserSafe(newOwnerBytes, signatures); 35 | 36 | vm.stopBroadcast(); 37 | } 38 | } -------------------------------------------------------------------------------- /script/preorder/DeployPreOrder.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | import "../../src/preorder/PreOrder.sol"; 9 | 10 | struct Proxy { 11 | address admin; 12 | address implementation; 13 | address proxy; 14 | } 15 | 16 | contract DeployPreOrder is Script { 17 | // Storages the addresses for the proxy deploy of the PreOrder contract 18 | Proxy PreOrderAddresses; 19 | 20 | // TODO: This is the mainnet contract controller gnosis. Be sure to change to the pre-order gnosis address 21 | address GnosisSafe = 0xe61B416113292696f9d4e4f7c1d42d5B2FB8BE79; 22 | address eEthToken = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; 23 | 24 | 25 | string baseURI = 26 | "https://etherfi-membership-metadata.s3.ap-southeast-1.amazonaws.com/cash-metadata/"; 27 | 28 | function run() public { 29 | // Pulling deployer info from the environment 30 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 31 | address deployerAddress = vm.addr(deployerPrivateKey); 32 | // Start broadcast with deployer as the signer 33 | vm.startBroadcast(deployerPrivateKey); 34 | 35 | // Deploy the implementation contract 36 | 37 | // Configuring the tiers 38 | PreOrder.TierConfig memory whales = PreOrder.TierConfig({ 39 | costWei: 10 ether, 40 | maxSupply: 200 41 | }); 42 | PreOrder.TierConfig memory chads = PreOrder.TierConfig({ 43 | costWei: 1 ether, 44 | maxSupply: 2000 45 | }); 46 | PreOrder.TierConfig memory wojak = PreOrder.TierConfig({ 47 | costWei: 0.1 ether, 48 | maxSupply: 20_000 49 | }); 50 | PreOrder.TierConfig memory pepe = PreOrder.TierConfig({ 51 | costWei: 0.01 ether, 52 | maxSupply: 200_000 53 | }); 54 | 55 | // TODO: Add more tiers when the tiers are offically set 56 | PreOrder.TierConfig[] memory tiers = new PreOrder.TierConfig[](4); 57 | tiers[0] = whales; 58 | tiers[1] = chads; 59 | tiers[2] = wojak; 60 | tiers[3] = pepe; 61 | 62 | // Deploy the implementation contract 63 | PreOrderAddresses.implementation = address(new PreOrder()); 64 | PreOrderAddresses.proxy = address( 65 | new ERC1967Proxy(PreOrderAddresses.implementation, "") 66 | ); 67 | 68 | PreOrder preOrder = PreOrder(payable(PreOrderAddresses.proxy)); 69 | preOrder.initialize( 70 | deployerAddress, 71 | GnosisSafe, 72 | deployerAddress, 73 | eEthToken, 74 | baseURI, 75 | tiers, 76 | deployerAddress 77 | ); 78 | vm.stopBroadcast(); 79 | 80 | console.log( 81 | "PreOrder implementation deployed at: ", 82 | PreOrderAddresses.implementation 83 | ); 84 | console.log("PreOrder proxy deployed at: ", PreOrderAddresses.proxy); 85 | console.log("PreOrder owner: ", deployerAddress); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /script/preorder/DeployPreOrderImpl.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | import "../../src/preorder/PreOrder.sol"; 9 | 10 | contract DeployPreOrderImpl is Script { 11 | function run() public { 12 | // Pulling deployer info from the environment 13 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 14 | // Start broadcast with deployer as the signer 15 | vm.startBroadcast(deployerPrivateKey); 16 | 17 | // Deploy the implementation contract 18 | 19 | // Deploy the implementation contract 20 | address preOrderImpl = address(new PreOrder()); 21 | 22 | vm.stopBroadcast(); 23 | 24 | console.log( 25 | "PreOrder implementation deployed at: ", 26 | preOrderImpl 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /script/preorder/UpdatePreOrder.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | import "../../src/preorder/PreOrder.sol"; 9 | 10 | struct Proxy { 11 | address admin; 12 | address implementation; 13 | address proxy; 14 | } 15 | 16 | contract UpdatePreOrder is Script { 17 | // includ the address of the proxy contract to be upgraded 18 | address constant PROXY_ADDRESS = 0x4E9fA586862183a944AA8A6E158af47CCaE544E2; 19 | 20 | function run() public { 21 | // Pulling deployer info from the environment 22 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 23 | address deployerAddress = vm.addr(deployerPrivateKey); 24 | // Start broadcast with deployer as the signer 25 | vm.startBroadcast(deployerPrivateKey); 26 | 27 | address impl = address(new PreOrder()); 28 | 29 | PreOrder proxy = PreOrder(payable(PROXY_ADDRESS)); 30 | 31 | bytes memory data = ""; 32 | 33 | proxy.upgradeToAndCall(impl, data); 34 | PreOrder(proxy).setFiatMinter(deployerAddress); 35 | 36 | vm.stopBroadcast(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /script/settlement-dispatcher/SettlementDispatcher.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | import {Script} from "forge-std/Script.sol"; 4 | import {SettlementDispatcher} from "../../src/settlement-dispatcher/SettlementDispatcher.sol"; 5 | import {UUPSProxy} from "../../src/UUPSProxy.sol"; 6 | import {CashDataProvider} from "../../src/utils/CashDataProvider.sol"; 7 | contract DeploySettlementDispatcher is Script { 8 | SettlementDispatcher settlementDispatcher; 9 | // Scroll 10 | address usdc = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; 11 | // https://stargateprotocol.gitbook.io/stargate/v/v2-developer-docs/technical-reference/mainnet-contracts#scroll 12 | address stargateUsdcPool = 0x3Fc69CC4A842838bCDC9499178740226062b14E4; 13 | // https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts 14 | uint32 optimismDestEid = 30111; 15 | uint48 accessControlDelay = 100; 16 | 17 | function run() public { 18 | // Pulling deployer info from the environment 19 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 20 | address deployerAddress = vm.addr(deployerPrivateKey); 21 | // Start broadcast with deployer as the signer 22 | vm.startBroadcast(deployerPrivateKey); 23 | 24 | address settlementDispatcherImpl = address(new SettlementDispatcher()); 25 | address[] memory tokens = new address[](1); 26 | tokens[0] = address(usdc); 27 | 28 | SettlementDispatcher.DestinationData[] memory destDatas = new SettlementDispatcher.DestinationData[](1); 29 | destDatas[0] = SettlementDispatcher.DestinationData({ 30 | destEid: optimismDestEid, 31 | destRecipient: deployerAddress, 32 | stargate: stargateUsdcPool 33 | }); 34 | 35 | settlementDispatcher = SettlementDispatcher(payable(address(new UUPSProxy( 36 | settlementDispatcherImpl, 37 | abi.encodeWithSelector( 38 | SettlementDispatcher.initialize.selector, 39 | accessControlDelay, 40 | deployerAddress, 41 | tokens, 42 | destDatas 43 | ) 44 | )))); 45 | 46 | CashDataProvider cashDataProvider = CashDataProvider(0x61D76fB1eb4645F30dE515d0483Bf3488F4a2B99); 47 | cashDataProvider.setSettlementDispatcher(address(settlementDispatcher)); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } -------------------------------------------------------------------------------- /script/settlement-dispatcher/UpgradeSettlementDispatcher.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils} from "../user-safe/Utils.sol"; 5 | import {SettlementDispatcher} from "../../src/settlement-dispatcher/SettlementDispatcher.sol"; 6 | import {stdJson} from "forge-std/StdJson.sol"; 7 | import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 8 | 9 | contract UpgradeSettlementDispatcher is Utils { 10 | using stdJson for string; 11 | 12 | function run() public { 13 | // Pulling deployer info from the environment 14 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 15 | // Start broadcast with deployer as the signer 16 | vm.startBroadcast(deployerPrivateKey); 17 | 18 | string memory deployments = readDeploymentFile(); 19 | 20 | address settlementDispatcherProxy = stdJson.readAddress( 21 | deployments, 22 | string.concat(".", "addresses", ".", "settlementDispatcherProxy") 23 | ); 24 | 25 | 26 | address settlementDispatcherImpl = address(new SettlementDispatcher()); 27 | UUPSUpgradeable(settlementDispatcherProxy).upgradeToAndCall(settlementDispatcherImpl, ""); 28 | 29 | vm.stopBroadcast(); 30 | } 31 | } -------------------------------------------------------------------------------- /script/top-up/TopUpDest.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils} from "../user-safe/Utils.sol"; 5 | import {TopUpDest} from "../../src/top-up/TopUpDest.sol"; 6 | import {UUPSProxy} from "../../src/UUPSProxy.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | 9 | contract DeployTopUpDest is Utils { 10 | TopUpDest topUpDest; 11 | 12 | address owner; 13 | address cashDataProvider; 14 | 15 | function run() public { 16 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 17 | vm.startBroadcast(deployerPrivateKey); 18 | 19 | string memory chainId = vm.toString(block.chainid); 20 | string memory file = string.concat(vm.projectRoot(), "/deployments/fixtures/top-up-fixtures.json"); 21 | string memory fixtures = vm.readFile(file); 22 | owner = stdJson.readAddress(fixtures, string.concat(".", chainId, ".", "owner")); 23 | 24 | string memory deployments = readDeploymentFile(); 25 | cashDataProvider = stdJson.readAddress( 26 | deployments, 27 | string.concat(".", "addresses", ".", "cashDataProviderProxy") 28 | ); 29 | 30 | address topUpDestImpl = address(new TopUpDest{salt: keccak256("topUpDestImpl")}()); 31 | 32 | bytes32 salt = keccak256("topUpDestProxy"); 33 | topUpDest = TopUpDest(payable(address(new UUPSProxy{salt: salt}(topUpDestImpl, "")))); 34 | topUpDest.initialize(owner, cashDataProvider); 35 | 36 | vm.stopBroadcast(); 37 | } 38 | } -------------------------------------------------------------------------------- /script/top-up/TopUpSource.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils} from "../user-safe/Utils.sol"; 5 | import {TopUpSource} from "../../src/top-up/TopUpSource.sol"; 6 | import {UUPSProxy} from "../../src/UUPSProxy.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | 9 | contract DeployTopUpSource is Utils { 10 | TopUpSource topUpSrc; 11 | 12 | address weth; 13 | address owner; 14 | address recoveryWallet; 15 | 16 | function run() public { 17 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 18 | vm.startBroadcast(deployerPrivateKey); 19 | 20 | string memory chainId = vm.toString(block.chainid); 21 | string memory file = string.concat(vm.projectRoot(), "/deployments/fixtures/top-up-fixtures.json"); 22 | string memory fixtures = vm.readFile(file); 23 | weth = stdJson.readAddress(fixtures, string.concat(".", chainId, ".", "weth")); 24 | recoveryWallet = stdJson.readAddress(fixtures, string.concat(".", chainId, ".", "recoveryWallet")); 25 | owner = vm.addr(deployerPrivateKey); 26 | 27 | address topUpSrcImpl = address(new TopUpSource{salt: keccak256("topUpSourceImpl")}()); 28 | 29 | bytes32 salt = keccak256("topUpSourceProxy"); 30 | topUpSrc = TopUpSource(payable(address(new UUPSProxy{salt: salt}(topUpSrcImpl, "")))); 31 | topUpSrc.initialize(weth, owner, recoveryWallet); 32 | 33 | vm.stopBroadcast(); 34 | } 35 | } -------------------------------------------------------------------------------- /script/top-up/bridge-adapters/OFTBridgeAdapter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils} from "../../user-safe/Utils.sol"; 5 | import {TopUpSource} from "../../../src/top-up/TopUpSource.sol"; 6 | import {UUPSProxy} from "../../../src/UUPSProxy.sol"; 7 | import {EtherFiOFTBridgeAdapter} from "../../../src/top-up/bridges/EtherFiOFTBridgeAdapter.sol"; 8 | 9 | contract DeployOFTBridgeAdapter is Utils { 10 | EtherFiOFTBridgeAdapter etherFiOFTBridgeAdapter; 11 | 12 | function run() public { 13 | // Pulling deployer info from the environment 14 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 15 | 16 | vm.startBroadcast(deployerPrivateKey); 17 | 18 | bytes32 salt = keccak256("etherFiOFTBridgeAdapter"); 19 | etherFiOFTBridgeAdapter = new EtherFiOFTBridgeAdapter{salt: salt}(); 20 | } 21 | } -------------------------------------------------------------------------------- /script/top-up/bridge-adapters/StargateAdapter.s.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: UNLICENSED 3 | pragma solidity ^0.8.24; 4 | 5 | import {Utils} from "../../user-safe/Utils.sol"; 6 | import {TopUpSource} from "../../../src/top-up/TopUpSource.sol"; 7 | import {UUPSProxy} from "../../../src/UUPSProxy.sol"; 8 | import {StargateAdapter} from "../../../src/top-up/bridges/StargateAdapter.sol"; 9 | 10 | contract DeployStargateAdapter is Utils { 11 | StargateAdapter stargateAdapter; 12 | 13 | function run() public { 14 | // Pulling deployer info from the environment 15 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 16 | 17 | vm.startBroadcast(deployerPrivateKey); 18 | bytes32 salt = keccak256("stargateAdapter"); 19 | stargateAdapter = new StargateAdapter{salt: salt}(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/user-safe/DeployUserSafe.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils, ChainConfig} from "./Utils.sol"; 5 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 6 | import {IUserSafe} from "../../src/interfaces/IUserSafe.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | import {UserSafeCore} from "../../src/user-safe/UserSafeCore.sol"; 9 | 10 | contract DeployUserSafe is Utils { 11 | UserSafeFactory userSafeFactory; 12 | IUserSafe ownerSafe; 13 | uint256 defaultDailySpendingLimit = 1000e6; 14 | uint256 defaultMonthlySpendingLimit = 10000e6; 15 | int256 timezoneOffset = 4 * 3600; // Dubai Timezone 16 | address ownerEoa; 17 | 18 | function run() public { 19 | // Pulling deployer info from the environment 20 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 21 | address deployer = vm.addr(deployerPrivateKey); 22 | 23 | if (ownerEoa == address(0)) ownerEoa = deployer; 24 | 25 | // Start broadcast with deployer as the signer 26 | vm.startBroadcast(deployerPrivateKey); 27 | 28 | string memory deployments = readDeploymentFile(); 29 | 30 | userSafeFactory = UserSafeFactory( 31 | stdJson.readAddress( 32 | deployments, 33 | string.concat(".", "addresses", ".", "userSafeFactoryProxy") 34 | ) 35 | ); 36 | 37 | bytes memory saltData = abi.encode("ownerSafe", block.timestamp); 38 | 39 | ownerSafe = IUserSafe( 40 | userSafeFactory.createUserSafe( 41 | saltData, 42 | abi.encodeWithSelector( 43 | UserSafeCore.initialize.selector, 44 | abi.encode(ownerEoa), 45 | defaultDailySpendingLimit, 46 | defaultMonthlySpendingLimit, 47 | timezoneOffset 48 | ) 49 | ) 50 | ); 51 | 52 | string memory parentObject = "parent object"; 53 | string memory deployedAddresses = "addresses"; 54 | 55 | vm.serializeAddress(deployedAddresses, "owner", ownerEoa); 56 | string memory addressOutput = vm.serializeAddress( 57 | deployedAddresses, 58 | "safe", 59 | address(ownerSafe) 60 | ); 61 | 62 | // serialize all the data 63 | string memory finalJson = vm.serializeString( 64 | parentObject, 65 | deployedAddresses, 66 | addressOutput 67 | ); 68 | 69 | writeUserSafeDeploymentFile(finalJson); 70 | vm.stopBroadcast(); 71 | } 72 | } -------------------------------------------------------------------------------- /script/user-safe/DeployUserSafeFactory.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils, ChainConfig} from "./Utils.sol"; 5 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 6 | import {stdJson} from "forge-std/StdJson.sol"; 7 | import {UUPSProxy} from "../../src/UUPSProxy.sol"; 8 | import {UserSafeCore} from "../../src/user-safe/UserSafeCore.sol"; 9 | import {UserSafeSetters} from "../../src/user-safe/UserSafeSetters.sol"; 10 | 11 | 12 | contract DeployUserSafeFactory is Utils { 13 | using stdJson for string; 14 | 15 | UserSafeCore userSafeCoreImpl; 16 | UserSafeSetters userSafeSettersImpl; 17 | UserSafeFactory userSafeFactory; 18 | address userSafeImpl; 19 | address cashDataProvider; 20 | address owner; 21 | 22 | function run() public { 23 | // Pulling deployer info from the environment 24 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 25 | address deployer = vm.addr(deployerPrivateKey); 26 | 27 | // Start broadcast with deployer as the signer 28 | vm.startBroadcast(deployerPrivateKey); 29 | 30 | string memory deployments = readDeploymentFile(); 31 | 32 | owner = deployer; 33 | userSafeImpl = stdJson.readAddress( 34 | deployments, 35 | string.concat(".", "addresses", ".", "userSafeImpl") 36 | ); 37 | 38 | cashDataProvider = stdJson.readAddress( 39 | deployments, 40 | string.concat(".", "addresses", ".", "cashDataProviderProxy") 41 | ); 42 | 43 | userSafeCoreImpl = new UserSafeCore(cashDataProvider); 44 | userSafeSettersImpl = new UserSafeSetters(cashDataProvider); 45 | address factoryImpl = address(new UserSafeFactory()); 46 | 47 | userSafeFactory = UserSafeFactory( 48 | address(new UUPSProxy( 49 | factoryImpl, 50 | abi.encodeWithSelector( 51 | UserSafeFactory.initialize.selector, 52 | address(userSafeImpl), 53 | owner, 54 | address(cashDataProvider), 55 | address(userSafeCoreImpl), 56 | address(userSafeSettersImpl) 57 | )) 58 | ) 59 | ); 60 | 61 | vm.stopBroadcast(); 62 | } 63 | } -------------------------------------------------------------------------------- /script/user-safe/UpgradeDebtManager.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils} from "./Utils.sol"; 5 | import {DebtManagerCore} from "../../src/debt-manager/DebtManagerCore.sol"; 6 | import {stdJson} from "forge-std/StdJson.sol"; 7 | import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 8 | 9 | contract UpgradeDebtManager is Utils { 10 | using stdJson for string; 11 | 12 | function run() public { 13 | // Pulling deployer info from the environment 14 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 15 | 16 | // Start broadcast with deployer as the signer 17 | vm.startBroadcast(deployerPrivateKey); 18 | 19 | string memory deployments = readDeploymentFile(); 20 | 21 | address debtManager = stdJson.readAddress( 22 | deployments, 23 | string.concat(".", "addresses", ".", "debtManagerProxy") 24 | ); 25 | 26 | address debtManagerImpl = address(new DebtManagerCore()); 27 | UUPSUpgradeable(debtManager).upgradeToAndCall(address(debtManagerImpl), ""); 28 | 29 | vm.stopBroadcast(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /script/user-safe/UpgradeUserSafeCore.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils, ChainConfig} from "./Utils.sol"; 5 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 6 | import {UserSafeCore} from "../../src/user-safe/UserSafeCore.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | 9 | contract UpgradeUserSafeCore is Utils { 10 | using stdJson for string; 11 | 12 | UserSafeFactory userSafeFactory; 13 | UserSafeCore userSafeCoreImpl; 14 | 15 | function run() public { 16 | // Pulling deployer info from the environment 17 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 18 | 19 | // Start broadcast with deployer as the signer 20 | vm.startBroadcast(deployerPrivateKey); 21 | 22 | string memory deployments = readDeploymentFile(); 23 | 24 | address cashDataProvider = stdJson.readAddress( 25 | deployments, 26 | string.concat(".", "addresses", ".", "cashDataProviderProxy") 27 | ); 28 | 29 | userSafeFactory = UserSafeFactory( 30 | stdJson.readAddress( 31 | deployments, 32 | string.concat(".", "addresses", ".", "userSafeFactory") 33 | ) 34 | ); 35 | 36 | userSafeCoreImpl = new UserSafeCore(address(cashDataProvider)); 37 | 38 | userSafeFactory.upgradeUserSafeCoreImpl(address(userSafeCoreImpl)); 39 | 40 | vm.stopBroadcast(); 41 | } 42 | } -------------------------------------------------------------------------------- /script/user-safe/UpgradeUserSafeSetters.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {Utils, ChainConfig} from "./Utils.sol"; 5 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 6 | import {UserSafeSetters} from "../../src/user-safe/UserSafeSetters.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | 9 | contract UpgradeUserSafeSetters is Utils { 10 | using stdJson for string; 11 | 12 | UserSafeFactory userSafeFactory; 13 | UserSafeSetters userSafeSettersImpl; 14 | 15 | function run() public { 16 | // Pulling deployer info from the environment 17 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 18 | 19 | // Start broadcast with deployer as the signer 20 | vm.startBroadcast(deployerPrivateKey); 21 | 22 | string memory deployments = readDeploymentFile(); 23 | 24 | address cashDataProvider = stdJson.readAddress( 25 | deployments, 26 | string.concat(".", "addresses", ".", "cashDataProviderProxy") 27 | ); 28 | 29 | userSafeFactory = UserSafeFactory( 30 | stdJson.readAddress( 31 | deployments, 32 | string.concat(".", "addresses", ".", "userSafeFactory") 33 | ) 34 | ); 35 | 36 | userSafeSettersImpl = new UserSafeSetters(address(cashDataProvider)); 37 | userSafeFactory.setUserSafeSettersImpl(address(userSafeSettersImpl)); 38 | 39 | vm.stopBroadcast(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/UUPSProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 5 | 6 | contract UUPSProxy is ERC1967Proxy { 7 | constructor( 8 | address _implementation, 9 | bytes memory _data 10 | ) ERC1967Proxy(_implementation, _data) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/adapters/aave-v3/EtherFiCashAaveV3Adapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IPool} from "@aave/interfaces/IPool.sol"; 5 | import {IPoolDataProvider} from "@aave/interfaces/IPoolDataProvider.sol"; 6 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {ICashDataProvider} from "../../interfaces/ICashDataProvider.sol"; 8 | import {IEtherFiCashAaveV3Adapter} from "../../interfaces/IEtherFiCashAaveV3Adapter.sol"; 9 | 10 | contract EtherFiCashAaveV3Adapter is IEtherFiCashAaveV3Adapter { 11 | using SafeERC20 for IERC20; 12 | 13 | // Address of the AaveV3 Pool contract 14 | IPool public immutable aaveV3Pool; 15 | // Address of the AaveV3 Pool Data provider 16 | IPoolDataProvider public immutable aaveV3PoolDataProvider; 17 | // Referral code for AaveV3 18 | uint16 public immutable aaveV3ReferralCode; 19 | // Interest rate mode -> Stable: 1, variable: 2 20 | uint256 public immutable interestRateMode; 21 | 22 | constructor( 23 | address _aaveV3Pool, 24 | address _aaveV3PoolDataProvider, 25 | uint16 _aaveV3ReferralCode, 26 | uint256 _interestRateMode 27 | ) { 28 | if (_interestRateMode != 1 && _interestRateMode != 2) 29 | revert InvalidRateMode(); 30 | 31 | aaveV3Pool = IPool(_aaveV3Pool); 32 | aaveV3PoolDataProvider = IPoolDataProvider(_aaveV3PoolDataProvider); 33 | aaveV3ReferralCode = _aaveV3ReferralCode; 34 | interestRateMode = _interestRateMode; 35 | } 36 | 37 | /** 38 | * @inheritdoc IEtherFiCashAaveV3Adapter 39 | */ 40 | function process( 41 | address assetToSupply, 42 | uint256 amountToSupply, 43 | address assetToBorrow, 44 | uint256 amountToBorrow 45 | ) external { 46 | _supply(assetToSupply, amountToSupply); 47 | 48 | if (!_getIsCollateral(assetToSupply)) 49 | aaveV3Pool.setUserUseReserveAsCollateral(assetToSupply, true); 50 | 51 | _borrow(assetToBorrow, amountToBorrow); 52 | 53 | emit AaveV3Process( 54 | assetToSupply, 55 | amountToSupply, 56 | assetToBorrow, 57 | amountToBorrow 58 | ); 59 | } 60 | 61 | /** 62 | * @inheritdoc IEtherFiCashAaveV3Adapter 63 | */ 64 | function supply(address asset, uint256 amount) external { 65 | _supply(asset, amount); 66 | 67 | if (!_getIsCollateral(asset)) 68 | aaveV3Pool.setUserUseReserveAsCollateral(asset, true); 69 | } 70 | 71 | /** 72 | * @inheritdoc IEtherFiCashAaveV3Adapter 73 | */ 74 | function borrow(address asset, uint256 amount) external { 75 | _borrow(asset, amount); 76 | } 77 | 78 | /** 79 | * @inheritdoc IEtherFiCashAaveV3Adapter 80 | */ 81 | function repay(address asset, uint256 amount) external { 82 | _repay(asset, amount); 83 | } 84 | 85 | /** 86 | * @inheritdoc IEtherFiCashAaveV3Adapter 87 | */ 88 | function withdraw(address asset, uint256 amount) external { 89 | _withdraw(asset, amount); 90 | } 91 | 92 | /** 93 | * @inheritdoc IEtherFiCashAaveV3Adapter 94 | */ 95 | function getAccountData( 96 | address user 97 | ) public view returns (AaveAccountData memory) { 98 | ( 99 | uint256 totalCollateralBase, 100 | uint256 totalDebtBase, 101 | uint256 availableBorrowsBase, 102 | uint256 currentLiquidationThreshold, 103 | uint256 ltv, 104 | uint256 healthFactor 105 | ) = aaveV3Pool.getUserAccountData(user); 106 | 107 | return 108 | AaveAccountData({ 109 | totalCollateralBase: totalCollateralBase, 110 | totalDebtBase: totalDebtBase, 111 | availableBorrowsBase: availableBorrowsBase, 112 | currentLiquidationThreshold: currentLiquidationThreshold, 113 | ltv: ltv, 114 | healthFactor: healthFactor 115 | }); 116 | } 117 | 118 | /** 119 | * @inheritdoc IEtherFiCashAaveV3Adapter 120 | */ 121 | function getDebt( 122 | address user, 123 | address token 124 | ) external view returns (uint256) { 125 | ( 126 | , 127 | uint256 stableDebt, 128 | uint256 variableDebt, 129 | , 130 | , 131 | , 132 | , 133 | , 134 | 135 | ) = aaveV3PoolDataProvider.getUserReserveData(token, user); 136 | return interestRateMode == 1 ? stableDebt : variableDebt; 137 | } 138 | 139 | /** 140 | * @inheritdoc IEtherFiCashAaveV3Adapter 141 | */ 142 | function getCollateralBalance( 143 | address user, 144 | address token 145 | ) external view returns (uint256 balance) { 146 | (balance, , , , , , , , ) = aaveV3PoolDataProvider.getUserReserveData( 147 | token, 148 | user 149 | ); 150 | } 151 | 152 | function _supply(address asset, uint256 amount) internal { 153 | IERC20(asset).safeIncreaseAllowance(address(aaveV3Pool), amount); 154 | aaveV3Pool.supply(asset, amount, address(this), aaveV3ReferralCode); 155 | } 156 | 157 | function _borrow(address asset, uint256 amount) internal { 158 | aaveV3Pool.borrow( 159 | asset, 160 | amount, 161 | interestRateMode, 162 | aaveV3ReferralCode, 163 | address(this) 164 | ); 165 | } 166 | 167 | function _repay(address asset, uint256 amount) internal { 168 | IERC20(asset).safeIncreaseAllowance(address(aaveV3Pool), amount); 169 | aaveV3Pool.repay(asset, amount, interestRateMode, address(this)); 170 | } 171 | 172 | function _withdraw(address token, uint256 amount) internal { 173 | aaveV3Pool.withdraw(token, amount, address(this)); 174 | } 175 | 176 | /** 177 | * @dev Checks if collateral is enabled for an asset 178 | * @param token token address of the asset.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) 179 | */ 180 | function _getIsCollateral( 181 | address token 182 | ) internal view returns (bool isCollateral) { 183 | (, , , , , , , , isCollateral) = aaveV3PoolDataProvider 184 | .getUserReserveData(token, address(this)); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/cash-wrapper-token/CashTokenWrapperFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; 5 | import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; 6 | import {CashWrappedERC20} from "./CashWrappedERC20.sol"; 7 | import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 8 | 9 | contract CashTokenWrapperFactory is UpgradeableBeacon { 10 | mapping (address inputToken => address wrappedToken) public cashWrappedToken; 11 | 12 | event WrappedTokenDeployed(address token); 13 | 14 | error WrappedTokenAlreadyExists(); 15 | error WrappedTokenDoesntExists(); 16 | 17 | constructor( 18 | address _implementation, 19 | address _owner 20 | ) UpgradeableBeacon(_implementation, _owner) {} 21 | 22 | function deployWrapper(address inputToken) external onlyOwner returns (address) { 23 | if (cashWrappedToken[inputToken] != address(0)) revert WrappedTokenAlreadyExists(); 24 | bytes memory data = abi.encodeWithSelector( 25 | CashWrappedERC20.initialize.selector, 26 | inputToken, 27 | string(abi.encodePacked("eCash ", IERC20Metadata(inputToken).name())), 28 | string(abi.encodePacked("ec", IERC20Metadata(inputToken).symbol())), 29 | IERC20Metadata(inputToken).decimals() 30 | ); 31 | 32 | address wrappedToken = address(new BeaconProxy(address(this), data)); 33 | cashWrappedToken[inputToken] = wrappedToken; 34 | 35 | emit WrappedTokenDeployed(wrappedToken); 36 | return wrappedToken; 37 | } 38 | 39 | function whitelistMinters( 40 | address inputToken, 41 | address[] calldata accounts, 42 | bool[] calldata whitelists 43 | ) external onlyOwner { 44 | if (cashWrappedToken[inputToken] == address(0)) revert WrappedTokenDoesntExists(); 45 | CashWrappedERC20(cashWrappedToken[inputToken]).whitelistMinters(accounts, whitelists); 46 | } 47 | 48 | function whitelistRecipients( 49 | address inputToken, 50 | address[] calldata accounts, 51 | bool[] calldata whitelists 52 | ) external onlyOwner { 53 | if (cashWrappedToken[inputToken] == address(0)) revert WrappedTokenDoesntExists(); 54 | CashWrappedERC20(cashWrappedToken[inputToken]).whitelistRecipients(accounts, whitelists); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/cash-wrapper-token/CashWrappedERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC20Upgradeable} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; 5 | import {ERC20PermitUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; 6 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; 8 | 9 | contract CashWrappedERC20 is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable { 10 | using SafeERC20 for IERC20; 11 | 12 | uint8 _decimals; 13 | address public baseToken; 14 | address public factory; 15 | mapping (address account => bool isMinter) public isWhitelistedMinter; 16 | mapping (address account => bool isValidRecipient) public isWhitelistedRecipient; 17 | 18 | event WhitelistMinters(address[] accounts, bool[] whitelists); 19 | event WhitelistRecipients(address[] accounts, bool[] whitelists); 20 | event Withdraw(address to, uint256 amount); 21 | 22 | error OnlyWhitelistedMinter(); 23 | error NotAWhitelistedRecipient(); 24 | error OnlyFactory(); 25 | error ArrayLengthMismatch(); 26 | 27 | constructor() { 28 | _disableInitializers(); 29 | } 30 | 31 | function initialize( 32 | address __baseToken, 33 | string memory __name, 34 | string memory __symbol, 35 | uint8 __decimals 36 | ) external initializer { 37 | __ERC20_init(__name, __symbol); 38 | __ERC20Permit_init(__name); 39 | _decimals = __decimals; 40 | baseToken = __baseToken; 41 | factory = msg.sender; 42 | } 43 | 44 | function decimals() public view override returns (uint8) { 45 | return _decimals; 46 | } 47 | 48 | function mint(address to, uint256 amount) external { 49 | if (!isWhitelistedMinter[msg.sender]) revert OnlyWhitelistedMinter(); 50 | if (!isWhitelistedRecipient[to]) revert NotAWhitelistedRecipient(); 51 | 52 | IERC20(baseToken).safeTransferFrom(msg.sender, address(this), amount); 53 | _mint(to, amount); 54 | } 55 | 56 | function withdraw(address to, uint256 amount) external { 57 | _burn(msg.sender, amount); 58 | IERC20(baseToken).safeTransfer(to, amount); 59 | emit Withdraw(to, amount); 60 | } 61 | 62 | function transfer(address to, uint256 amount) public override returns (bool) { 63 | if (!isWhitelistedRecipient[to]) revert NotAWhitelistedRecipient(); 64 | super.transfer(to, amount); 65 | return true; 66 | } 67 | 68 | function transferFrom(address from, address to, uint256 amount) public override returns (bool) { 69 | if (!isWhitelistedRecipient[to]) revert NotAWhitelistedRecipient(); 70 | super.transferFrom(from, to, amount); 71 | return true; 72 | } 73 | 74 | function whitelistMinters(address[] calldata accounts, bool[] calldata whitelists) external onlyFactory { 75 | _whitelist(isWhitelistedMinter, accounts, whitelists); 76 | emit WhitelistMinters(accounts, whitelists); 77 | } 78 | 79 | function whitelistRecipients(address[] calldata accounts, bool[] calldata whitelists) external onlyFactory { 80 | _whitelist(isWhitelistedRecipient, accounts, whitelists); 81 | emit WhitelistRecipients(accounts, whitelists); 82 | } 83 | 84 | function _whitelist( 85 | mapping (address => bool) storage whitelist, 86 | address[] calldata accounts, 87 | bool[] calldata whitelists 88 | ) internal { 89 | uint256 len = accounts.length; 90 | if (len != whitelists.length) revert ArrayLengthMismatch(); 91 | 92 | for (uint256 i = 0; i < len; ) { 93 | whitelist[accounts[i]] = whitelists[i]; 94 | unchecked { 95 | ++i; 96 | } 97 | } 98 | } 99 | 100 | function _onlyFactory() internal view { 101 | if (msg.sender != factory) revert OnlyFactory(); 102 | } 103 | 104 | modifier onlyFactory() { 105 | _onlyFactory(); 106 | _; 107 | } 108 | } -------------------------------------------------------------------------------- /src/debt-manager/DebtManagerInitializer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | /** 5 | * @title DebtManagerInitializer 6 | */ 7 | 8 | import {DebtManagerStorage, ICashDataProvider} from "./DebtManagerStorage.sol"; 9 | 10 | contract DebtManagerInitializer is DebtManagerStorage { 11 | function initialize( 12 | address __owner, 13 | address __cashDataProvider 14 | ) external initializer { 15 | __UUPSUpgradeable_init(); 16 | __ReentrancyGuardTransient_init_unchained(); 17 | __AccessControlDefaultAdminRules_init(5 * 60, __owner); 18 | _grantRole(ADMIN_ROLE, __owner); 19 | 20 | _cashDataProvider = ICashDataProvider(__cashDataProvider); 21 | } 22 | } -------------------------------------------------------------------------------- /src/interfaces/IAggregatorV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | interface IAggregatorV3 { 5 | function decimals() external view returns (uint8); 6 | 7 | function description() external view returns (string memory); 8 | 9 | function version() external view returns (uint256); 10 | 11 | function getRoundData( 12 | uint80 _roundId 13 | ) 14 | external 15 | view 16 | returns ( 17 | uint80 roundId, 18 | int256 answer, 19 | uint256 startedAt, 20 | uint256 updatedAt, 21 | uint80 answeredInRound 22 | ); 23 | 24 | function latestRoundData() 25 | external 26 | view 27 | returns ( 28 | uint80 roundId, 29 | int256 answer, 30 | uint256 startedAt, 31 | uint256 updatedAt, 32 | uint80 answeredInRound 33 | ); 34 | 35 | function latestAnswer() external view returns (int256); 36 | } 37 | -------------------------------------------------------------------------------- /src/interfaces/ICrossChainMessenger.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | // facilitate pair-wise communication 5 | interface ICrossChainMessenger { 6 | function send(address asset, uint256 amount) external; 7 | 8 | function setCrossChainMessenger(address newMessenger) external; 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.20; 5 | 6 | /** 7 | * @dev Interface of the ERC20 standard as defined in the EIP. 8 | */ 9 | interface IERC20 { 10 | /** 11 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 12 | * another (`to`). 13 | * 14 | * Note that `value` may be zero. 15 | */ 16 | event Transfer(address indexed from, address indexed to, uint256 value); 17 | 18 | /** 19 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 20 | * a call to {approve}. `value` is the new allowance. 21 | */ 22 | event Approval(address indexed owner, address indexed spender, uint256 value); 23 | 24 | /** 25 | * @dev Returns the value of tokens in existence. 26 | */ 27 | function totalSupply() external view returns (uint256); 28 | 29 | /** 30 | * @dev Returns the value of tokens owned by `account`. 31 | */ 32 | function balanceOf(address account) external view returns (uint256); 33 | 34 | /** 35 | * @dev Moves a `value` amount of tokens from the caller's account to `to`. 36 | * 37 | * Returns a boolean value indicating whether the operation succeeded. 38 | * 39 | * Emits a {Transfer} event. 40 | */ 41 | function transfer(address to, uint256 value) external returns (bool); 42 | 43 | /** 44 | * @dev Returns the remaining number of tokens that `spender` will be 45 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 46 | * zero by default. 47 | * 48 | * This value changes when {approve} or {transferFrom} are called. 49 | */ 50 | function allowance(address owner, address spender) external view returns (uint256); 51 | 52 | /** 53 | * @dev Sets a `value` amount of tokens as the allowance of `spender` over the 54 | * caller's tokens. 55 | * 56 | * Returns a boolean value indicating whether the operation succeeded. 57 | * 58 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 59 | * that someone may use both the old and the new allowance by unfortunate 60 | * transaction ordering. One possible solution to mitigate this race 61 | * condition is to first reduce the spender's allowance to 0 and set the 62 | * desired value afterwards: 63 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 64 | * 65 | * Emits an {Approval} event. 66 | */ 67 | function approve(address spender, uint256 value) external returns (bool); 68 | 69 | /** 70 | * @dev Moves a `value` amount of tokens from `from` to `to` using the 71 | * allowance mechanism. `value` is then deducted from the caller's 72 | * allowance. 73 | * 74 | * Returns a boolean value indicating whether the operation succeeded. 75 | * 76 | * Emits a {Transfer} event. 77 | */ 78 | function transferFrom(address from, address to, uint256 value) external returns (bool); 79 | } 80 | -------------------------------------------------------------------------------- /src/interfaces/IEtherFiCashAaveV3Adapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) 3 | pragma solidity ^0.8.24; 4 | interface IEtherFiCashAaveV3Adapter { 5 | struct AaveAccountData { 6 | uint256 totalCollateralBase; 7 | uint256 totalDebtBase; 8 | uint256 availableBorrowsBase; 9 | uint256 currentLiquidationThreshold; // ltv liquidation threshold in basis points 10 | uint256 ltv; // loan to value ration in basis points 11 | uint256 healthFactor; 12 | } 13 | 14 | event AaveV3Process( 15 | address assetToSupply, 16 | uint256 amountToSupply, 17 | address assetToBorrow, 18 | uint256 amountToBorrow 19 | ); 20 | 21 | error InvalidRateMode(); 22 | 23 | /** 24 | * @notice Function to supply and borrow via Aave V3. 25 | * @param assetToSupply Address of the asset to supply. 26 | * @param amountToSupply Amount of the asset to supply. 27 | * @param assetToBorrow Address of the asset to borrow. 28 | * @param amountToBorrow Amount of the asset to borrow. 29 | */ 30 | function process( 31 | address assetToSupply, 32 | uint256 amountToSupply, 33 | address assetToBorrow, 34 | uint256 amountToBorrow 35 | ) external; 36 | 37 | /** 38 | * @notice Function to supply funds to Aave V3. 39 | * @param asset Address of the asset to supply. 40 | * @param amount Amount of the asset to supply. 41 | */ 42 | function supply(address asset, uint256 amount) external; 43 | 44 | /** 45 | * @notice Function to borrow funds from Aave V3. 46 | * @param asset Address of the asset to borrow. 47 | * @param amount Amount of the asset to borrow. 48 | */ 49 | function borrow(address asset, uint256 amount) external; 50 | 51 | /** 52 | * @notice Function to repay funds to Aave V3. 53 | * @param asset Address of the asset to repay. 54 | * @param amount Amount of the asset to repay. 55 | */ 56 | function repay(address asset, uint256 amount) external; 57 | 58 | /** 59 | * @notice Function to withdraw funds from Aave V3. 60 | * @param asset Address of the asset to withdraw. 61 | * @param amount Amount of the asset to withdraw. 62 | */ 63 | function withdraw(address asset, uint256 amount) external; 64 | 65 | /** 66 | * @notice Function to get the account data for a user. 67 | * @param user Address of the user. 68 | * @return AaveAccountData struct. 69 | */ 70 | function getAccountData( 71 | address user 72 | ) external view returns (AaveAccountData memory); 73 | 74 | /** 75 | * @dev Get total debt balance for an asset. 76 | * @param user Address of the user. 77 | * @param token Address of the debt token. 78 | * @return debt Amount of debt. 79 | */ 80 | function getDebt( 81 | address user, 82 | address token 83 | ) external view returns (uint256 debt); 84 | 85 | /** 86 | * @dev Get total collateral balance for an asset. 87 | * @param user Address of the user. 88 | * @param token Address of the token used as collateral. 89 | * @return balance Amount fo collateral balance. 90 | */ 91 | function getCollateralBalance( 92 | address user, 93 | address token 94 | ) external view returns (uint256 balance); 95 | } 96 | -------------------------------------------------------------------------------- /src/interfaces/IOneInch.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// 1Inch swap data 7 | struct SwapDescription { 8 | IERC20 srcToken; // contract address of a token to sell 9 | IERC20 dstToken; // contract address of a token to buy 10 | address payable srcReceiver; 11 | address payable dstReceiver; // Receiver of destination currency. default: fromAddress 12 | uint256 amount; 13 | uint256 minReturnAmount; 14 | uint256 flags; 15 | } 16 | 17 | /// @title Interface for making arbitrary calls during swap 18 | interface IAggregationExecutor { 19 | /// @notice propagates information about original msg.sender and executes arbitrary data 20 | function execute(address msgSender) external payable returns (uint256); // 0x4b64e492 21 | } 22 | 23 | interface IOneInchRouterV6 { 24 | type Address is uint256; 25 | 26 | /** 27 | * @notice Performs a swap, delegating all calls encoded in `data` to `executor`. See tests for usage examples. 28 | * @dev Router keeps 1 wei of every token on the contract balance for gas optimisations reasons. 29 | * This affects first swap of every token by leaving 1 wei on the contract. 30 | * @param executor Aggregation executor that executes calls described in `data`. 31 | * @param desc Swap description. 32 | * @param data Encoded calls that `caller` should execute in between of swaps. 33 | * @return returnAmount Resulting token amount. 34 | * @return spentAmount Source token amount. 35 | */ 36 | function swap( 37 | IAggregationExecutor executor, 38 | SwapDescription calldata desc, 39 | bytes calldata data 40 | ) external payable returns (uint256 returnAmount, uint256 spentAmount); 41 | 42 | /** 43 | * @notice Swaps `amount` of the specified `token` for another token using an Unoswap-compatible exchange's pool, 44 | * sending the resulting tokens to the `to` address, with a minimum return specified by `minReturn`. 45 | * @param to The address to receive the swapped tokens. 46 | * @param token The address of the token to be swapped. 47 | * @param amount The amount of tokens to be swapped. 48 | * @param minReturn The minimum amount of tokens to be received after the swap. 49 | * @param dex The address of the Unoswap-compatible exchange's pool. 50 | * @return returnAmount The actual amount of tokens received after the swap. 51 | */ 52 | function unoswapTo( 53 | uint256 to, 54 | uint256 token, 55 | uint256 amount, 56 | uint256 minReturn, 57 | uint256 dex 58 | ) external returns (uint256 returnAmount); 59 | 60 | /** 61 | * @notice Swaps `amount` of the specified `token` for another token using two Unoswap-compatible exchange pools (`dex` and `dex2`) sequentially, 62 | * sending the resulting tokens to the `to` address, with a minimum return specified by `minReturn`. 63 | * @param to The address to receive the swapped tokens. 64 | * @param token The address of the token to be swapped. 65 | * @param amount The amount of tokens to be swapped. 66 | * @param minReturn The minimum amount of tokens to be received after the swap. 67 | * @param dex The address of the first Unoswap-compatible exchange's pool. 68 | * @param dex2 The address of the second Unoswap-compatible exchange's pool. 69 | * @return returnAmount The actual amount of tokens received after the swap through both pools. 70 | */ 71 | function unoswapTo2( 72 | uint256 to, 73 | uint256 token, 74 | uint256 amount, 75 | uint256 minReturn, 76 | uint256 dex, 77 | uint256 dex2 78 | ) external returns (uint256 returnAmount); 79 | } 80 | -------------------------------------------------------------------------------- /src/interfaces/IOpenOcean.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | struct OpenOceanSwapDescription { 7 | IERC20 srcToken; 8 | IERC20 dstToken; 9 | address srcReceiver; 10 | address dstReceiver; 11 | uint256 amount; 12 | uint256 minReturnAmount; 13 | uint256 guaranteedAmount; 14 | uint256 flags; 15 | address referrer; 16 | bytes permit; 17 | } 18 | 19 | /// @title Interface for making arbitrary calls during swap 20 | interface IOpenOceanCaller { 21 | struct CallDescription { 22 | uint256 target; 23 | uint256 gasLimit; 24 | uint256 value; 25 | bytes data; 26 | } 27 | 28 | function makeCall(CallDescription memory desc) external; 29 | 30 | function makeCalls(CallDescription[] memory desc) external payable; 31 | } 32 | 33 | interface IOpenOceanRouter { 34 | /// @notice Performs a swap, delegating all calls encoded in `data` to `executor`. 35 | function swap( 36 | IOpenOceanCaller caller, 37 | OpenOceanSwapDescription calldata desc, 38 | IOpenOceanCaller.CallDescription[] calldata calls 39 | ) external returns (uint256 returnAmount); 40 | } 41 | -------------------------------------------------------------------------------- /src/interfaces/IPriceProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | interface IPriceProvider { 5 | error UnknownToken(); 6 | /** 7 | * @notice Function to get the price of a token in USD 8 | * @return Price with 6 decimals 9 | */ 10 | function price(address token) external view returns (uint256); 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/IStargate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.24; 3 | 4 | // Solidity does not support splitting import across multiple lines 5 | // solhint-disable-next-line max-line-length 6 | import { IOFT, SendParam, MessagingFee, MessagingReceipt, OFTReceipt } from "./IOFT.sol"; 7 | 8 | /// @notice Stargate implementation type. 9 | enum StargateType { 10 | Pool, 11 | OFT 12 | } 13 | 14 | /// @notice Ticket data for bus ride. 15 | struct Ticket { 16 | uint72 ticketId; 17 | bytes passengerBytes; 18 | } 19 | 20 | /// @title Interface for Stargate. 21 | /// @notice Defines an API for sending tokens to destination chains. 22 | interface IStargate is IOFT { 23 | /// @dev This function is same as `send` in OFT interface but returns the ticket data if in the bus ride mode, 24 | /// which allows the caller to ride and drive the bus in the same transaction. 25 | function sendToken( 26 | SendParam calldata _sendParam, 27 | MessagingFee calldata _fee, 28 | address _refundAddress 29 | ) external payable returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt, Ticket memory ticket); 30 | 31 | /// @notice Returns the Stargate implementation type. 32 | function stargateType() external pure returns (StargateType); 33 | 34 | function token() external view returns (address); 35 | } -------------------------------------------------------------------------------- /src/interfaces/ISwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface ISwapper { 5 | /** 6 | * @notice Strategist swaps assets sitting in the contract of the `assetHolder`. 7 | * @param _fromAsset The token address of the asset being sold by the vault. 8 | * @param _toAsset The token address of the asset being purchased by the vault. 9 | * @param _fromAssetAmount The amount of assets being sold by the vault. 10 | * @param _minToAssetAmount The minimum amount of assets to be purchased. 11 | * @param _guaranteedAmount The guaranteed amount of output (only for openocean). 12 | * @param _data RLP encoded executor address and bytes data. This is re-encoded tx.data from 1Inch swap API 13 | */ 14 | function swap( 15 | address _fromAsset, 16 | address _toAsset, 17 | uint256 _fromAssetAmount, 18 | uint256 _minToAssetAmount, 19 | uint256 _guaranteedAmount, 20 | bytes calldata _data 21 | ) external returns (uint256 toAssetAmount); 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/IUserRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | // User 5 | // Account 6 | // 7 | interface IUserRegistry { 8 | struct User { 9 | address account; 10 | } 11 | // any other fields can be added 12 | 13 | function AccountOf(address user) external view returns (User memory); 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | function withdraw(uint wad) external; 7 | } -------------------------------------------------------------------------------- /src/interfaces/IWeETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | interface IWeETH { 5 | function getEETHByWeETH( 6 | uint256 _weETHAmount 7 | ) external view returns (uint256); 8 | } -------------------------------------------------------------------------------- /src/libraries/AaveLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IEtherFiCashAaveV3Adapter} from "../interfaces/IEtherFiCashAaveV3Adapter.sol"; 6 | 7 | library AaveLib { 8 | error AaveAdapterNotSet(); 9 | error InvalidMarketOperationType(); 10 | 11 | enum MarketOperationType { 12 | Supply, 13 | Borrow, 14 | Repay, 15 | Withdraw, 16 | SupplyAndBorrow 17 | } 18 | 19 | function aaveOperation( 20 | address aaveV3Adapter, 21 | uint8 marketOperationType, 22 | bytes calldata data 23 | ) internal { 24 | if (aaveV3Adapter == address(0)) revert AaveAdapterNotSet(); 25 | 26 | if (marketOperationType == uint8(MarketOperationType.Supply)) { 27 | (address token, uint256 amount) = abi.decode( 28 | data, 29 | (address, uint256) 30 | ); 31 | supplyOnAave(aaveV3Adapter, token, amount); 32 | } else if (marketOperationType == uint8(MarketOperationType.Borrow)) { 33 | (address token, uint256 amount) = abi.decode( 34 | data, 35 | (address, uint256) 36 | ); 37 | borrowFromAave(aaveV3Adapter, token, amount); 38 | } else if (marketOperationType == uint8(MarketOperationType.Repay)) { 39 | (address token, uint256 amount) = abi.decode( 40 | data, 41 | (address, uint256) 42 | ); 43 | repayOnAave(aaveV3Adapter, token, amount); 44 | } else if (marketOperationType == uint8(MarketOperationType.Withdraw)) { 45 | (address token, uint256 amount) = abi.decode( 46 | data, 47 | (address, uint256) 48 | ); 49 | withdrawFromAave(aaveV3Adapter, token, amount); 50 | } else if ( 51 | marketOperationType == uint8(MarketOperationType.SupplyAndBorrow) 52 | ) { 53 | ( 54 | address tokenToSupply, 55 | uint256 amountToSupply, 56 | address tokenToBorrow, 57 | uint256 amountToBorrow 58 | ) = abi.decode(data, (address, uint256, address, uint256)); 59 | supplyAndBorrowOnAave( 60 | aaveV3Adapter, 61 | tokenToSupply, 62 | amountToSupply, 63 | tokenToBorrow, 64 | amountToBorrow 65 | ); 66 | } else revert InvalidMarketOperationType(); 67 | } 68 | 69 | function supplyAndBorrowOnAave( 70 | address aaveV3Adapter, 71 | address tokenToSupply, 72 | uint256 amountToSupply, 73 | address tokenToBorrow, 74 | uint256 amountToBorrow 75 | ) internal { 76 | delegateCall( 77 | aaveV3Adapter, 78 | abi.encodeWithSelector( 79 | IEtherFiCashAaveV3Adapter.process.selector, 80 | tokenToSupply, 81 | amountToSupply, 82 | tokenToBorrow, 83 | amountToBorrow 84 | ) 85 | ); 86 | } 87 | 88 | function supplyOnAave( 89 | address aaveV3Adapter, 90 | address token, 91 | uint256 amount 92 | ) internal { 93 | delegateCall( 94 | aaveV3Adapter, 95 | abi.encodeWithSelector( 96 | IEtherFiCashAaveV3Adapter.supply.selector, 97 | token, 98 | amount 99 | ) 100 | ); 101 | } 102 | 103 | function borrowFromAave( 104 | address aaveV3Adapter, 105 | address token, 106 | uint256 amount 107 | ) internal { 108 | delegateCall( 109 | aaveV3Adapter, 110 | abi.encodeWithSelector( 111 | IEtherFiCashAaveV3Adapter.borrow.selector, 112 | token, 113 | amount 114 | ) 115 | ); 116 | } 117 | 118 | function repayOnAave( 119 | address aaveV3Adapter, 120 | address token, 121 | uint256 amount 122 | ) internal { 123 | delegateCall( 124 | aaveV3Adapter, 125 | abi.encodeWithSelector( 126 | IEtherFiCashAaveV3Adapter.repay.selector, 127 | token, 128 | amount 129 | ) 130 | ); 131 | } 132 | 133 | function withdrawFromAave( 134 | address aaveV3Adapter, 135 | address token, 136 | uint256 amount 137 | ) internal { 138 | delegateCall( 139 | aaveV3Adapter, 140 | abi.encodeWithSelector( 141 | IEtherFiCashAaveV3Adapter.withdraw.selector, 142 | token, 143 | amount 144 | ) 145 | ); 146 | } 147 | 148 | function delegateCall( 149 | address target, 150 | bytes memory data 151 | ) internal returns (bytes memory result) { 152 | require(target != address(this), "delegatecall to self"); 153 | 154 | // solhint-disable-next-line no-inline-assembly 155 | assembly ("memory-safe") { 156 | // Perform delegatecall to the target contract 157 | let success := delegatecall( 158 | gas(), 159 | target, 160 | add(data, 0x20), 161 | mload(data), 162 | 0, 163 | 0 164 | ) 165 | 166 | // Get the size of the returned data 167 | let size := returndatasize() 168 | 169 | // Allocate memory for the return data 170 | result := mload(0x40) 171 | 172 | // Set the length of the return data 173 | mstore(result, size) 174 | 175 | // Copy the return data to the allocated memory 176 | returndatacopy(add(result, 0x20), 0, size) 177 | 178 | // Update the free memory pointer 179 | mstore(0x40, add(result, add(0x20, size))) 180 | 181 | if iszero(success) { 182 | revert(result, returndatasize()) 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/libraries/ArrayDeDupTransientLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.24; 3 | 4 | library ArrayDeDupTransient { 5 | error DuplicateTokenFound(); 6 | 7 | function checkDuplicates(address[] calldata tokens) internal { 8 | bytes4 errorSelector = DuplicateTokenFound.selector; 9 | // Use assembly to interact with transient storage 10 | assembly ("memory-safe") { 11 | // Iterate through the tokens array 12 | for { let i := 0 } lt(i, tokens.length) { i := add(i, 1) } 13 | { 14 | // Load the current token address 15 | let token := calldataload(add(tokens.offset, mul(i, 0x20))) 16 | 17 | // Check if the token has been seen before 18 | if tload(token) { 19 | // If found, revert with custom error 20 | mstore(0x00, errorSelector) 21 | revert(0x00, 0x04) 22 | } 23 | 24 | // Mark the token as seen 25 | tstore(token, 1) 26 | } 27 | 28 | for { let i := 0 } lt(i, tokens.length) { i := add(i, 1) } 29 | { 30 | let token := calldataload(add(tokens.offset, mul(i, 0x20))) 31 | tstore(token, 0) 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/libraries/Base64Url.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Encode (without '=' padding) 6 | * @author evmbrahmin, adapted from hiromin's Base64URL libraries 7 | */ 8 | library Base64Url { 9 | /** 10 | * @dev Base64Url Encoding Table 11 | */ 12 | string internal constant ENCODING_TABLE = 13 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 14 | 15 | function encode(bytes memory data) internal pure returns (string memory) { 16 | if (data.length == 0) return ""; 17 | 18 | // Load the table into memory 19 | string memory table = ENCODING_TABLE; 20 | 21 | string memory result = new string(4 * ((data.length + 2) / 3)); 22 | 23 | // @solidity memory-safe-assembly 24 | assembly ("memory-safe") { 25 | let tablePtr := add(table, 1) 26 | let resultPtr := add(result, 32) 27 | 28 | for { 29 | let dataPtr := data 30 | let endPtr := add(data, mload(data)) 31 | } lt(dataPtr, endPtr) { 32 | 33 | } { 34 | dataPtr := add(dataPtr, 3) 35 | let input := mload(dataPtr) 36 | 37 | mstore8( 38 | resultPtr, 39 | mload(add(tablePtr, and(shr(18, input), 0x3F))) 40 | ) 41 | resultPtr := add(resultPtr, 1) 42 | 43 | mstore8( 44 | resultPtr, 45 | mload(add(tablePtr, and(shr(12, input), 0x3F))) 46 | ) 47 | resultPtr := add(resultPtr, 1) 48 | 49 | mstore8( 50 | resultPtr, 51 | mload(add(tablePtr, and(shr(6, input), 0x3F))) 52 | ) 53 | resultPtr := add(resultPtr, 1) 54 | 55 | mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) 56 | resultPtr := add(resultPtr, 1) 57 | } 58 | 59 | // Remove the padding adjustment logic 60 | switch mod(mload(data), 3) 61 | case 1 { 62 | // Adjust for the last byte of data 63 | resultPtr := sub(resultPtr, 2) 64 | } 65 | case 2 { 66 | // Adjust for the last two bytes of data 67 | resultPtr := sub(resultPtr, 1) 68 | } 69 | 70 | // Set the correct length of the result string 71 | mstore(result, sub(resultPtr, add(result, 32))) 72 | } 73 | 74 | return result; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/libraries/EIP1271SignatureUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.24; 3 | 4 | import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; 5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | /** 8 | * @title Library of utilities for making EIP1271-compliant signature checks. 9 | * @author Layr Labs, Inc. 10 | */ 11 | library EIP1271SignatureUtils { 12 | // bytes4(keccak256("isValidSignature(bytes32,bytes)") 13 | bytes4 internal constant EIP1271_MAGICVALUE = 0x1626ba7e; 14 | 15 | error InvalidSigner(); 16 | error InvalidERC1271Signer(); 17 | 18 | /** 19 | * @notice Checks @param signature is a valid signature of @param digestHash from @param signer. 20 | * If the `signer` contains no code -- i.e. it is not (yet, at least) a contract address, then checks using standard ECDSA logic 21 | * Otherwise, passes on the signature to the signer to verify the signature and checks that it returns the `EIP1271_MAGICVALUE`. 22 | */ 23 | function checkSignature_EIP1271( 24 | bytes32 digestHash, 25 | address signer, 26 | bytes memory signature 27 | ) internal view { 28 | /** 29 | * check validity of signature: 30 | * 1) if `signer` is an EOA, then `signature` must be a valid ECDSA signature from `signer`, 31 | * indicating their intention for this action 32 | * 2) if `signer` is a contract, then `signature` must will be checked according to EIP-1271 33 | */ 34 | if (isContract(signer)) { 35 | if ( 36 | IERC1271(signer).isValidSignature(digestHash, signature) != 37 | EIP1271_MAGICVALUE 38 | ) revert InvalidERC1271Signer(); 39 | } else { 40 | if (ECDSA.recover(digestHash, signature) != signer) 41 | revert InvalidSigner(); 42 | } 43 | } 44 | 45 | function isValidSignature_EIP1271( 46 | bytes32 digestHash, 47 | address signer, 48 | bytes memory signature 49 | ) internal view returns (bool) { 50 | /** 51 | * check validity of signature: 52 | * 1) if `signer` is an EOA, then `signature` must be a valid ECDSA signature from `signer`, 53 | * indicating their intention for this action 54 | * 2) if `signer` is a contract, then `signature` must will be checked according to EIP-1271 55 | */ 56 | if (isContract(signer)) { 57 | if ( 58 | IERC1271(signer).isValidSignature(digestHash, signature) != 59 | EIP1271_MAGICVALUE 60 | ) return false; 61 | 62 | return true; 63 | } else { 64 | if (ECDSA.recover(digestHash, signature) != signer) return false; 65 | 66 | return true; 67 | } 68 | } 69 | 70 | function isContract(address account) internal view returns (bool) { 71 | uint256 size; 72 | assembly ("memory-safe") { 73 | size := extcodesize(account) 74 | } 75 | return size > 0; 76 | } 77 | } -------------------------------------------------------------------------------- /src/libraries/OwnerLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.24; 3 | 4 | library OwnerLib { 5 | struct OwnerObject { 6 | address ethAddr; 7 | uint256 x; 8 | uint256 y; 9 | } 10 | 11 | error OnlyOwner(); 12 | error OwnerCannotBeZero(); 13 | 14 | function getOwnerObject( 15 | bytes memory _ownerBytes 16 | ) internal pure returns (OwnerObject memory) { 17 | if (_ownerBytes.length == 32) { 18 | address addr; 19 | assembly ("memory-safe") { 20 | addr := mload(add(_ownerBytes, 32)) 21 | } 22 | 23 | return OwnerObject({ethAddr: addr, x: 0, y: 0}); 24 | } 25 | 26 | (uint256 x, uint256 y) = abi.decode(_ownerBytes, (uint256, uint256)); 27 | return OwnerObject({ethAddr: address(0), x: x, y: y}); 28 | } 29 | 30 | function getOwnerObject( 31 | address _owner 32 | ) internal pure returns (OwnerObject memory) { 33 | return OwnerObject({ethAddr: _owner, x: 0, y: 0}); 34 | } 35 | 36 | function _onlyOwner(bytes memory _ownerBytes) internal view { 37 | if (_ownerBytes.length != 32) revert OnlyOwner(); 38 | 39 | address __owner; 40 | assembly ("memory-safe") { 41 | __owner := mload(add(_ownerBytes, 32)) 42 | } 43 | 44 | if (msg.sender != __owner) revert OnlyOwner(); 45 | } 46 | 47 | function _ownerNotZero(OwnerObject memory owner) internal pure { 48 | if (owner.ethAddr == address(0) && owner.x == 0 && owner.y == 0) 49 | revert OwnerCannotBeZero(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/libraries/SignatureUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.24; 3 | 4 | import {EIP1271SignatureUtils} from "./EIP1271SignatureUtils.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {OwnerLib} from "./OwnerLib.sol"; 7 | import {WebAuthn} from "./WebAuthn.sol"; 8 | 9 | /** 10 | * @title Signature Utils 11 | */ 12 | library SignatureUtils { 13 | using EIP1271SignatureUtils for bytes32; 14 | using MessageHashUtils for bytes32; 15 | 16 | error InvalidWebAuthSignature(); 17 | 18 | function verifySig( 19 | bytes32 hash, 20 | OwnerLib.OwnerObject memory owner, 21 | bytes calldata signature 22 | ) internal view { 23 | if (owner.ethAddr != address(0)) 24 | hash.toEthSignedMessageHash().checkSignature_EIP1271(owner.ethAddr, signature); 25 | else { 26 | WebAuthn.WebAuthnAuth memory auth = abi.decode( 27 | signature, 28 | (WebAuthn.WebAuthnAuth) 29 | ); 30 | 31 | if ( 32 | !WebAuthn.verify({ 33 | challenge: abi.encode(hash), 34 | requireUV: false, 35 | webAuthnAuth: auth, 36 | x: owner.x, 37 | y: owner.y 38 | }) 39 | ) revert InvalidWebAuthSignature(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/libraries/TimeLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | library TimeLib { 5 | // Function to find the timestamp for the start of the next day (00:00 in the user's time zone) 6 | function getStartOfNextDay(uint256 timestamp, int256 timezoneOffset) internal pure returns (uint64) { 7 | // Adjust the timestamp for the time zone offset 8 | int256 adjustedTimestamp = int256(timestamp) + timezoneOffset; 9 | 10 | // Calculate the current day in the adjusted time zone 11 | uint256 currentDay = uint256(adjustedTimestamp / 1 days); 12 | 13 | // Calculate the start of the next day in the adjusted time zone 14 | uint256 startOfNextDay = (currentDay + 1) * 1 days; 15 | 16 | // Adjust the result back to the user's local time zone 17 | return uint64(uint256(int256(startOfNextDay) - timezoneOffset)); 18 | } 19 | 20 | // Function to find the start of the next week (Monday 00:00 in the user's time zone) 21 | function getStartOfNextWeek(uint256 timestamp, int256 timezoneOffset) internal pure returns (uint64) { 22 | // Adjust the timestamp for the time zone offset 23 | int256 adjustedTimestamp = int256(timestamp) + timezoneOffset; 24 | 25 | // Calculate the current day of the week (0 = Monday, 6 = Sunday) 26 | uint256 dayOfWeek = (uint256(adjustedTimestamp) / 1 days + 3) % 7; 27 | 28 | // Calculate the number of days until the next Monday 29 | uint256 daysUntilNextMonday = (dayOfWeek == 0) ? 7 : (7 - dayOfWeek); 30 | 31 | // Calculate the start of the next week in the adjusted time zone 32 | uint256 startOfNextWeek = ((uint256(adjustedTimestamp) / 1 days) + daysUntilNextMonday) * 1 days; 33 | 34 | // Adjust the result back to the user's local time zone 35 | return uint64(uint256(int256(startOfNextWeek) - timezoneOffset)); 36 | } 37 | 38 | // Function to find the start of the next month (in the user's time zone) 39 | function getStartOfNextMonth(uint256 timestamp, int256 timezoneOffset) internal pure returns (uint64) { 40 | // Adjust the timestamp for the time zone offset 41 | int256 adjustedTimestamp = int256(timestamp) + timezoneOffset; 42 | 43 | // Get the current date in the adjusted time zone 44 | (uint16 year, uint8 month, ) = _daysToDate(uint256(adjustedTimestamp) / 1 days); 45 | 46 | // Increment the month and adjust the year if necessary 47 | month += 1; 48 | if (month > 12) { 49 | month = 1; 50 | year += 1; 51 | } 52 | 53 | // Calculate the start of the next month in the adjusted time zone 54 | uint256 startOfNextMonth = _daysFromDate(year, month, 1) * 1 days; 55 | 56 | // Adjust the result back to the user's local time zone 57 | return uint64(uint256(int256(startOfNextMonth) - timezoneOffset)); 58 | } 59 | 60 | // Internal function to calculate days from date 61 | function _daysFromDate(uint16 year, uint8 month, uint8 day) internal pure returns (uint256) { 62 | int256 _year = int256(uint256(year)); 63 | int256 _month = int256(uint256(month)); 64 | int256 _day = int256(uint256(day)); 65 | 66 | int256 __days = _day 67 | - 32075 68 | + 1461 * (_year + 4800 + (_month - 14) / 12) / 4 69 | + 367 * (_month - 2 - (_month - 14) / 12 * 12) / 12 70 | - 3 * ((_year + 4900 + (_month - 14) / 12) / 100) / 4 71 | - 2440588; 72 | 73 | return uint256(__days); 74 | } 75 | 76 | // Internal function to convert days to date 77 | function _daysToDate(uint256 _days) internal pure returns (uint16 year, uint8 month, uint8 day) { 78 | int256 __days = int256(_days); 79 | 80 | int256 L = __days + 68569 + 2440588; 81 | int256 N = 4 * L / 146097; 82 | L = L - (146097 * N + 3) / 4; 83 | int256 _year = 4000 * (L + 1) / 1461001; 84 | L = L - 1461 * _year / 4 + 31; 85 | int256 _month = 80 * L / 2447; 86 | int256 _day = L - 2447 * _month / 80; 87 | L = _month / 11; 88 | _month = _month + 2 - 12 * L; 89 | _year = 100 * (N - 49) + _year + L; 90 | 91 | year = uint16(uint256(_year)); 92 | month = uint8(uint256(_month)); 93 | day = uint8(uint256(_day)); 94 | } 95 | } -------------------------------------------------------------------------------- /src/mocks/MockAaveAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {IEtherFiCashAaveV3Adapter} from "../interfaces/IEtherFiCashAaveV3Adapter.sol"; 6 | 7 | contract MockAave { 8 | using SafeERC20 for IERC20; 9 | 10 | uint256 totalCollateral; 11 | uint256 totalDebt; 12 | uint256 availableBorrowsBase; 13 | uint256 currentLiquidationThreshold; 14 | uint256 ltv; 15 | uint256 healthFactor; 16 | 17 | function supply( 18 | address, // asset 19 | uint256 amount 20 | ) external { 21 | totalCollateral += amount; 22 | } 23 | 24 | function borrow( 25 | address, // asset 26 | uint256 amount 27 | ) external { 28 | totalDebt += amount; 29 | } 30 | 31 | function repay( 32 | address, // asset 33 | uint256 amount 34 | ) external { 35 | totalDebt -= amount; 36 | } 37 | 38 | function withdraw( 39 | address, // asset 40 | uint256 amount 41 | ) external { 42 | totalCollateral -= amount; 43 | } 44 | 45 | function getAccountData( 46 | address // user 47 | ) external view returns (IEtherFiCashAaveV3Adapter.AaveAccountData memory) { 48 | return 49 | IEtherFiCashAaveV3Adapter.AaveAccountData({ 50 | totalCollateralBase: totalCollateral, 51 | totalDebtBase: totalDebt, 52 | availableBorrowsBase: availableBorrowsBase, 53 | currentLiquidationThreshold: currentLiquidationThreshold, 54 | ltv: ltv, 55 | healthFactor: healthFactor 56 | }); 57 | } 58 | 59 | function getDebt( 60 | address, // user 61 | address // token 62 | ) external view returns (uint256 debt) { 63 | return totalDebt; 64 | } 65 | 66 | function getCollateralBalance( 67 | address, // user 68 | address // token 69 | ) external view returns (uint256 balance) { 70 | return totalCollateral; 71 | } 72 | } 73 | 74 | contract MockAaveAdapter is IEtherFiCashAaveV3Adapter { 75 | using SafeERC20 for IERC20; 76 | MockAave public immutable aave; 77 | 78 | constructor() { 79 | aave = new MockAave(); 80 | } 81 | 82 | function process( 83 | address assetToSupply, 84 | uint256 amountToSupply, 85 | address assetToBorrow, 86 | uint256 amountToBorrow 87 | ) external { 88 | aave.supply(assetToSupply, amountToSupply); 89 | aave.borrow(assetToBorrow, amountToBorrow); 90 | 91 | emit AaveV3Process( 92 | assetToSupply, 93 | amountToSupply, 94 | assetToBorrow, 95 | amountToBorrow 96 | ); 97 | } 98 | 99 | function supply(address asset, uint256 amount) external { 100 | aave.supply(asset, amount); 101 | } 102 | 103 | function borrow(address asset, uint256 amount) external { 104 | aave.borrow(asset, amount); 105 | } 106 | 107 | function repay(address asset, uint256 amount) external { 108 | aave.repay(asset, amount); 109 | } 110 | 111 | function withdraw(address asset, uint256 amount) external { 112 | aave.withdraw(asset, amount); 113 | } 114 | 115 | function getAccountData( 116 | address user 117 | ) external view returns (AaveAccountData memory) { 118 | return aave.getAccountData(user); 119 | } 120 | 121 | function getDebt( 122 | address user, 123 | address token 124 | ) external view returns (uint256 debt) { 125 | return aave.getDebt(user, token); 126 | } 127 | 128 | function getCollateralBalance( 129 | address user, 130 | address token 131 | ) external view returns (uint256 balance) { 132 | return aave.getCollateralBalance(user, token); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | uint8 __decimals; 8 | constructor( 9 | string memory _name, 10 | string memory _symbol, 11 | uint8 _decimals 12 | ) ERC20(_name, _symbol) { 13 | __decimals = _decimals; 14 | 15 | _mint(msg.sender, 100000000 ether); 16 | } 17 | 18 | function decimals() public view override returns (uint8) { 19 | return __decimals; 20 | } 21 | 22 | function mint(address to, uint256 amount) public { 23 | _mint(to, amount); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mocks/MockPriceProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | /** 5 | * @title MockPriceProvider 6 | */ 7 | contract MockPriceProvider { 8 | uint256 _price; 9 | mapping (address => bool) public isStableToken; 10 | 11 | constructor(uint256 __price, address stableToken) { 12 | _price = __price; 13 | isStableToken[stableToken] = true; 14 | } 15 | 16 | function setStableToken(address token) external { 17 | isStableToken[token] = true; 18 | } 19 | 20 | function setPrice(uint256 __price) external { 21 | _price = __price; 22 | } 23 | 24 | function price( 25 | address token 26 | ) public view returns (uint256) { 27 | if (isStableToken[token]) return 1e6; 28 | return _price; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/mocks/MockSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | 6 | contract MockSwapper { 7 | using SafeERC20 for IERC20; 8 | 9 | error InvalidMinToAssetAmount(); 10 | 11 | function swap( 12 | address, // _fromAsset 13 | address _toAsset, 14 | uint256, // _fromAssetAmount 15 | uint256 _minToAssetAmount, 16 | uint256, // _guaranteedAmount 17 | bytes calldata // _data 18 | ) external returns (uint256 toAssetAmount) { 19 | // more than a million usdc swap we won't support on test 20 | if (_minToAssetAmount > 1e12) revert InvalidMinToAssetAmount(); 21 | IERC20(_toAsset).safeTransfer(msg.sender, _minToAssetAmount); 22 | return _minToAssetAmount; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/mocks/UserSafeV2Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {UserSafeCore} from "../user-safe/UserSafeCore.sol"; 5 | 6 | contract UserSafeV2Mock is UserSafeCore { 7 | constructor(address __cashDataProvider) UserSafeCore(__cashDataProvider) {} 8 | 9 | function version() external pure returns (uint256) { 10 | return 2; 11 | } 12 | } -------------------------------------------------------------------------------- /src/preorder/interfaces/IL2DebtManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | interface IL2DebtManager { 5 | // -------------- 6 | // Admin functions 7 | // -------------- 8 | 9 | function setAdmin(address newAdmin) external; 10 | function setLiquidationThreshold(uint256 newThreshold) external; 11 | 12 | function liquidate(address user) external; 13 | 14 | // function transferUSDC(uint256 amount, address to) external; 15 | 16 | // -------------- 17 | // User functions 18 | // -------------- 19 | 20 | // Deposit of Collateral eETH from Users 21 | function depositEETH(address user, uint256 amount) external; 22 | 23 | // Debt and collateral 24 | function collateralOf(address user) external view returns (uint256); 25 | function borrowingOf(address user) external view returns (uint256); 26 | function debtRatioOf(address user) external view returns (uint256); 27 | 28 | // Repayment 29 | function repayWithUSDC(uint256 repayUsdcAmount) external; 30 | function repayWithEETH(uint256 repayUsdcAmount) external; 31 | 32 | // -------------- 33 | // View functions 34 | // -------------- 35 | 36 | // Query for liquidated collateral 37 | function liquidatedCollateralAmount() external view returns (uint256); 38 | function totalCollateralAmount() external view returns (uint256); 39 | function totalBorrowingAmount() external view returns (uint256); 40 | 41 | // Additional helper functions may be added here as necessary 42 | } 43 | -------------------------------------------------------------------------------- /src/top-up/bridges/BridgeAdapterBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; 5 | 6 | abstract contract BridgeAdapterBase { 7 | using Math for uint256; 8 | 9 | address ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 10 | 11 | error InsufficientNativeFee(); 12 | error InsufficientMinAmount(); 13 | 14 | function deductSlippage(uint256 amount, uint256 slippage) internal pure returns (uint256) { 15 | return amount.mulDiv(10000 - slippage, 10000); 16 | } 17 | 18 | function bridge( 19 | address token, 20 | uint256 amount, 21 | address destRecipient, 22 | uint256 maxSlippage, 23 | bytes calldata additionalData 24 | ) external payable virtual; 25 | 26 | function getBridgeFee( 27 | address token, 28 | uint256 amount, 29 | address destRecipient, 30 | uint256 maxSlippage, 31 | bytes calldata additionalData 32 | ) external view virtual returns (address, uint256); 33 | } -------------------------------------------------------------------------------- /src/top-up/bridges/EtherFiOFTBridgeAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import { SendParam, MessagingFee, OFTLimit, OFTFeeDetail, OFTReceipt, MessagingReceipt, SendParam, IOFT } from "../../interfaces/IOFT.sol"; 5 | import { BridgeAdapterBase } from "./BridgeAdapterBase.sol"; 6 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | 8 | contract EtherFiOFTBridgeAdapter is BridgeAdapterBase { 9 | using SafeERC20 for IERC20; 10 | 11 | // https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts 12 | uint32 public constant DEST_EID_SCROLL = 30214; 13 | 14 | event BridgeOFT(address token, uint256 amount, MessagingReceipt messageReceipt, OFTReceipt oftReceipt); 15 | 16 | error AmountOutOfOFTLimit(); 17 | 18 | function bridge( 19 | address token, 20 | uint256 amount, 21 | address destRecipient, 22 | uint256 maxSlippage, 23 | bytes calldata additionalData 24 | ) external payable override { 25 | IOFT oftAdapter = IOFT(abi.decode(additionalData, (address))); 26 | uint256 minAmount = deductSlippage(amount, maxSlippage); 27 | 28 | SendParam memory sendParam = SendParam({ 29 | dstEid: DEST_EID_SCROLL, 30 | to: bytes32(uint256(uint160(destRecipient))), 31 | amountLD: amount, 32 | minAmountLD: minAmount, 33 | extraOptions: hex"0003", 34 | composeMsg: new bytes(0), 35 | oftCmd: new bytes(0) 36 | }); 37 | 38 | MessagingFee memory messagingFee = oftAdapter.quoteSend(sendParam, false); 39 | if (address(this).balance < messagingFee.nativeFee) revert InsufficientNativeFee(); 40 | 41 | if (oftAdapter.approvalRequired()) IERC20(token).forceApprove(address(oftAdapter), amount); 42 | 43 | (MessagingReceipt memory messageReceipt, OFTReceipt memory oftReceipt) = oftAdapter.send{ value: messagingFee.nativeFee }(sendParam, messagingFee, payable(address(this))); 44 | if (oftReceipt.amountReceivedLD < minAmount) revert InsufficientMinAmount(); 45 | 46 | emit BridgeOFT(token, amount, messageReceipt, oftReceipt); 47 | } 48 | 49 | function getBridgeFee( 50 | address, 51 | uint256 amount, 52 | address destRecipient, 53 | uint256 maxSlippage, 54 | bytes calldata additionalData 55 | ) external view override returns (address, uint256) { 56 | IOFT oftAdapter = IOFT(abi.decode(additionalData, (address))); 57 | uint256 minAmount = deductSlippage(amount, maxSlippage); 58 | 59 | SendParam memory sendParam = SendParam({ 60 | dstEid: DEST_EID_SCROLL, 61 | to: bytes32(uint256(uint160(destRecipient))), 62 | amountLD: amount, 63 | minAmountLD: minAmount, 64 | extraOptions: hex"0003", 65 | composeMsg: new bytes(0), 66 | oftCmd: new bytes(0) 67 | }); 68 | 69 | MessagingFee memory messagingFee = oftAdapter.quoteSend(sendParam, false); 70 | 71 | return (ETH, messagingFee.nativeFee); 72 | } 73 | } -------------------------------------------------------------------------------- /src/top-up/bridges/StargateAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import { IStargate, Ticket } from "../../interfaces/IStargate.sol"; 5 | import { MessagingFee, OFTReceipt, SendParam } from "../../interfaces/IOFT.sol"; 6 | import { BridgeAdapterBase } from "./BridgeAdapterBase.sol"; 7 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | 9 | contract StargateAdapter is BridgeAdapterBase { 10 | using SafeERC20 for IERC20; 11 | 12 | event BridgeViaStargate(address token, uint256 amount, Ticket ticket); 13 | 14 | // https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts 15 | uint32 public constant DEST_EID_SCROLL = 30214; 16 | 17 | error InvalidStargatePool(); 18 | 19 | function bridge( 20 | address token, 21 | uint256 amount, 22 | address destRecipient, 23 | uint256 maxSlippage, 24 | bytes calldata additionalData 25 | ) external payable override { 26 | address stargatePool = abi.decode(additionalData, (address)); 27 | uint256 minAmount = deductSlippage(amount, maxSlippage); 28 | 29 | (uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee, address poolToken) = 30 | prepareRideBus(stargatePool, amount, destRecipient, minAmount); 31 | 32 | if (address(this).balance < valueToSend) revert InsufficientNativeFee(); 33 | 34 | if (poolToken != address(0)) { 35 | if (poolToken != token) revert InvalidStargatePool(); 36 | IERC20(token).forceApprove(stargatePool, amount); 37 | } 38 | (, , Ticket memory ticket) = IStargate(stargatePool).sendToken{ value: valueToSend }(sendParam, messagingFee, payable(address(this))); 39 | emit BridgeViaStargate(token, amount, ticket); 40 | } 41 | 42 | // from https://stargateprotocol.gitbook.io/stargate/v/v2-developer-docs/integrate-with-stargate/how-to-swap#ride-the-bus 43 | function prepareRideBus( 44 | address stargate, 45 | uint256 amount, 46 | address destRecipient, 47 | uint256 minAmount 48 | ) public view returns (uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee, address poolToken) { 49 | sendParam = SendParam({ 50 | dstEid: DEST_EID_SCROLL, 51 | to: bytes32(uint256(uint160(destRecipient))), 52 | amountLD: amount, 53 | minAmountLD: amount, 54 | extraOptions: new bytes(0), 55 | composeMsg: new bytes(0), 56 | oftCmd: new bytes(1) 57 | }); 58 | 59 | (, , OFTReceipt memory receipt) = IStargate(stargate).quoteOFT(sendParam); 60 | sendParam.minAmountLD = receipt.amountReceivedLD; 61 | if (minAmount > receipt.amountReceivedLD) revert InsufficientMinAmount(); 62 | 63 | messagingFee = IStargate(stargate).quoteSend(sendParam, false); 64 | valueToSend = messagingFee.nativeFee; 65 | poolToken = IStargate(stargate).token(); 66 | if (poolToken == address(0)) { 67 | valueToSend += sendParam.amountLD; 68 | } 69 | } 70 | 71 | function getBridgeFee( 72 | address, 73 | uint256 amount, 74 | address destRecipient, 75 | uint256 maxSlippage, 76 | bytes calldata additionalData 77 | ) external view override returns (address, uint256) { 78 | address stargatePool = abi.decode(additionalData, (address)); 79 | uint256 minAmount = deductSlippage(amount, maxSlippage); 80 | 81 | (, , MessagingFee memory messagingFee, ) = 82 | prepareRideBus(stargatePool, amount, destRecipient, minAmount); 83 | 84 | return (ETH, messagingFee.nativeFee); 85 | } 86 | } -------------------------------------------------------------------------------- /src/user-safe/UserSafeFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; 5 | import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; 6 | import {CREATE3} from "solady/utils/CREATE3.sol"; 7 | import {ICashDataProvider} from "../interfaces/ICashDataProvider.sol"; 8 | import {UUPSUpgradeable, Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 9 | import {AccessControlDefaultAdminRulesUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; 10 | import {UserSafeSetters} from "./UserSafeSetters.sol"; 11 | import {UserSafeCore} from "./UserSafeCore.sol"; 12 | 13 | /** 14 | * @title UserSafeFactory 15 | * @author ether.fi [shivam@ether.fi] 16 | * @notice Factory to deploy User Safe contracts 17 | */ 18 | contract UserSafeFactory is Initializable, UUPSUpgradeable, AccessControlDefaultAdminRulesUpgradeable { 19 | bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); 20 | address public cashDataProvider; 21 | address public beacon; 22 | address public userSafeSettersImpl; 23 | 24 | event UserSafeDeployed(address indexed safe); 25 | event CashDataProviderSet(address oldProvider, address newProvider); 26 | event UserSafeSettersImplSet(address oldUserSafeSettersImpl, address newUserSafeSettersImpl); 27 | event BeaconSet(address oldBeacon, address newBeacon); 28 | 29 | error InvalidValue(); 30 | error SafeAddressDifferentFromDetermined(); 31 | 32 | constructor() { 33 | _disableInitializers(); 34 | } 35 | 36 | function initialize( 37 | address _owner, 38 | address _cashDataProvider, 39 | address _userSafeCoreImpl, 40 | address _userSafeSettersImpl 41 | ) external initializer { 42 | __AccessControlDefaultAdminRules_init(5 * 60, _owner); 43 | _grantRole(ADMIN_ROLE, _owner); 44 | beacon = address(new UpgradeableBeacon(_userSafeCoreImpl, address(this))); 45 | cashDataProvider = _cashDataProvider; 46 | userSafeSettersImpl = _userSafeSettersImpl; 47 | } 48 | 49 | function setCashDataProvider(address _cashDataProvider) external onlyRole(DEFAULT_ADMIN_ROLE) { 50 | if (_cashDataProvider == address(0)) revert InvalidValue(); 51 | emit CashDataProviderSet(cashDataProvider, _cashDataProvider); 52 | cashDataProvider = _cashDataProvider; 53 | } 54 | 55 | function setUserSafeSettersImpl(address _userSafeSettersImpl) external onlyRole(DEFAULT_ADMIN_ROLE) { 56 | if (_userSafeSettersImpl == address(0)) revert InvalidValue(); 57 | emit UserSafeSettersImplSet(userSafeSettersImpl, _userSafeSettersImpl); 58 | userSafeSettersImpl = _userSafeSettersImpl; 59 | } 60 | 61 | function setBeacon(address _beacon) external onlyRole(DEFAULT_ADMIN_ROLE) { 62 | if (_beacon == address(0)) revert InvalidValue(); 63 | emit BeaconSet(beacon, _beacon); 64 | beacon = _beacon; 65 | } 66 | 67 | function getUserSafeAddress(bytes memory saltData, bytes memory data) external view returns (address) { 68 | return CREATE3.predictDeterministicAddress(keccak256(abi.encode(saltData, data))); 69 | } 70 | 71 | /** 72 | * @notice Function to deploy a new User Safe. 73 | * @param data Initialize function data to be passed while deploying a user safe. 74 | * @return Address of the user safe. 75 | */ 76 | function createUserSafe(bytes memory saltData, bytes memory data) external onlyRole(ADMIN_ROLE) returns (address) { 77 | address safe = this.getUserSafeAddress(saltData, data); 78 | ICashDataProvider(cashDataProvider).whitelistUserSafe(safe); 79 | 80 | address deployedSafe = address( 81 | CREATE3.deployDeterministic( 82 | abi.encodePacked( 83 | type(BeaconProxy).creationCode, 84 | abi.encode(beacon, data) 85 | ), 86 | keccak256(abi.encode(saltData, data)) 87 | ) 88 | ); 89 | 90 | if (deployedSafe != safe) revert SafeAddressDifferentFromDetermined(); 91 | 92 | emit UserSafeDeployed(safe); 93 | return safe; 94 | } 95 | 96 | function upgradeUserSafeCoreImpl(address newImplementation) external onlyRole(DEFAULT_ADMIN_ROLE) { 97 | UpgradeableBeacon(beacon).upgradeTo(newImplementation); 98 | } 99 | 100 | function _authorizeUpgrade( 101 | address newImplementation 102 | ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} 103 | } -------------------------------------------------------------------------------- /src/user-safe/UserSafeLens.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe} from "../interfaces/IUserSafe.sol"; 5 | import {ICashDataProvider} from "../interfaces/ICashDataProvider.sol"; 6 | import {IL2DebtManager} from "../interfaces/IL2DebtManager.sol"; 7 | import {IPriceProvider} from "../interfaces/IPriceProvider.sol"; 8 | import {DebtManagerStorage} from "../debt-manager/DebtManagerStorage.sol"; 9 | import {UUPSUpgradeable, Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 10 | import {AccessControlDefaultAdminRulesUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; 11 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 12 | 13 | contract UserSafeLens is Initializable, UUPSUpgradeable, AccessControlDefaultAdminRulesUpgradeable { 14 | using Math for uint256; 15 | 16 | struct UserSafeData { 17 | IUserSafe.Mode mode; 18 | uint256 incomingCreditModeStartTime; 19 | IL2DebtManager.TokenData[] collateralBalances; 20 | IL2DebtManager.TokenData[] borrows; 21 | IL2DebtManager.TokenData[] tokenPrices; 22 | IUserSafe.WithdrawalRequest withdrawalRequest; 23 | uint256 totalCollateral; 24 | uint256 totalBorrow; 25 | uint256 maxBorrow; 26 | uint256 creditMaxSpend; 27 | uint256 debitMaxSpend; 28 | uint256 spendingLimitAllowance; 29 | uint256 totalCashbackEarnedInUsd; 30 | uint256 pendingCashbackInUsd; 31 | } 32 | 33 | ICashDataProvider public cashDataProvider; 34 | 35 | event CashDataProviderSet(address oldProvider, address newProvider); 36 | 37 | error InvalidValue(); 38 | 39 | constructor() { 40 | _disableInitializers(); 41 | } 42 | 43 | function initialize(address owner, address _cashDataProvider) external initializer { 44 | __UUPSUpgradeable_init(); 45 | __AccessControlDefaultAdminRules_init(5 * 60, owner); 46 | 47 | cashDataProvider = ICashDataProvider(_cashDataProvider); 48 | } 49 | 50 | function setCashDataProvider(address _cashDataProvider) external onlyRole(DEFAULT_ADMIN_ROLE) { 51 | if (_cashDataProvider == address(0)) revert InvalidValue(); 52 | emit CashDataProviderSet(address(cashDataProvider), _cashDataProvider); 53 | cashDataProvider = ICashDataProvider(_cashDataProvider); 54 | } 55 | 56 | function getUserSafeData(address user) external view returns (UserSafeData memory userData) { 57 | IUserSafe userSafe = IUserSafe(user); 58 | IL2DebtManager debtManager = IL2DebtManager(cashDataProvider.etherFiCashDebtManager()); 59 | 60 | ( 61 | userData.collateralBalances, 62 | userData.totalCollateral, 63 | userData.borrows, 64 | userData.totalBorrow 65 | ) = debtManager.getUserCurrentState(address(userSafe)); 66 | 67 | userData.withdrawalRequest = userSafe.pendingWithdrawalRequest(); 68 | userData.maxBorrow = debtManager.getMaxBorrowAmount(address(user), true); 69 | 70 | address[] memory supportedTokens = debtManager.getCollateralTokens(); 71 | uint256 len = supportedTokens.length; 72 | userData.tokenPrices = new IL2DebtManager.TokenData[](len); 73 | IPriceProvider priceProvider = IPriceProvider(cashDataProvider.priceProvider()); 74 | 75 | for (uint256 i = 0; i < len; ) { 76 | userData.tokenPrices[i].token = supportedTokens[i]; 77 | userData.tokenPrices[i].amount = priceProvider.price(supportedTokens[i]); 78 | unchecked { 79 | ++i; 80 | } 81 | } 82 | 83 | (userData.creditMaxSpend, userData.debitMaxSpend, userData.spendingLimitAllowance) = userSafe.maxCanSpend(debtManager.getBorrowTokens()[0]); 84 | userData.mode = userSafe.mode(); 85 | userData.incomingCreditModeStartTime = userSafe.incomingCreditModeStartTime(); 86 | userData.totalCashbackEarnedInUsd = userSafe.totalCashbackEarnedInUsd(); 87 | userData.pendingCashbackInUsd = userSafe.pendingCashback(); 88 | } 89 | 90 | function _authorizeUpgrade( 91 | address newImplementation 92 | ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} 93 | } -------------------------------------------------------------------------------- /src/utils/ReentrancyGuardTransientUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.24; 4 | 5 | import {StorageSlot} from "./StorageSlot.sol"; 6 | import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; 7 | 8 | /** 9 | * @dev Variant of {ReentrancyGuard} that uses transient storage. 10 | * 11 | * NOTE: This variant only works on networks where EIP-1153 is available. 12 | */ 13 | abstract contract ReentrancyGuardTransientUpgradeable is Initializable { 14 | using StorageSlot for *; 15 | 16 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff)) 17 | bytes32 private constant REENTRANCY_GUARD_STORAGE = 18 | 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; 19 | 20 | /** 21 | * @dev Unauthorized reentrant call. 22 | */ 23 | error ReentrancyGuardReentrantCall(); 24 | 25 | /** 26 | * @dev Prevents a contract from calling itself, directly or indirectly. 27 | * Calling a `nonReentrant` function from another `nonReentrant` 28 | * function is not supported. It is possible to prevent this from happening 29 | * by making the `nonReentrant` function external, and making it call a 30 | * `private` function that does the actual work. 31 | */ 32 | modifier nonReentrant() { 33 | _nonReentrantBefore(); 34 | _; 35 | _nonReentrantAfter(); 36 | } 37 | 38 | function __ReentrancyGuardTransient_init() internal onlyInitializing { 39 | } 40 | 41 | function __ReentrancyGuardTransient_init_unchained() internal onlyInitializing { 42 | } 43 | function _nonReentrantBefore() private { 44 | // On the first call to nonReentrant, _status will be NOT_ENTERED 45 | if (_reentrancyGuardEntered()) { 46 | revert ReentrancyGuardReentrantCall(); 47 | } 48 | 49 | // Any calls to nonReentrant after this point will fail 50 | REENTRANCY_GUARD_STORAGE.asBoolean().tstore(true); 51 | } 52 | 53 | function _nonReentrantAfter() private { 54 | REENTRANCY_GUARD_STORAGE.asBoolean().tstore(false); 55 | } 56 | 57 | /** 58 | * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a 59 | * `nonReentrant` function in the call stack. 60 | */ 61 | function _reentrancyGuardEntered() internal view returns (bool) { 62 | return REENTRANCY_GUARD_STORAGE.asBoolean().tload(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/utils/Swapper1InchV6.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | /** 5 | * @notice 1Inch Pathfinder V6 implementation of the general ISwapper interface. 6 | * @author ether.fi [shivam@ether.fi] 7 | * @dev It is possible that dust token amounts are left in this contract after a swap. 8 | * This can happen with some tokens that don't send the full transfer amount. 9 | * These dust amounts can build up over time and be used by anyone who calls the `swap` function. 10 | */ 11 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 12 | import {IAggregationExecutor, IOneInchRouterV6, SwapDescription} from "../interfaces/IOneInch.sol"; 13 | import {ISwapper} from "../interfaces/ISwapper.sol"; 14 | 15 | contract Swapper1InchV6 is ISwapper { 16 | using SafeERC20 for IERC20; 17 | 18 | /// @notice 1Inch router contract to give allowance to perform swaps 19 | address public immutable swapRouter; 20 | 21 | // swap(address,(address,address,address,address,uint256,uint256,uint256),bytes) 22 | bytes4 internal constant SWAP_SELECTOR = 0x07ed2379; 23 | // unoswapTo(uint256,uint256,uint256,uint256,uint256) 24 | bytes4 internal constant UNOSWAP_TO_SELECTOR = 0xe2c95c82; 25 | // unoswapTo2(uint256,uint256,uint256,uint256,uint256,uint256) 26 | bytes4 internal constant UNOSWAP_TO_2_SELECTOR = 0xea76dddf; 27 | 28 | error UnsupportedSwapFunction(); 29 | error OutputLessThanMinAmount(); 30 | 31 | constructor(address _swapRouter, address[] memory _assets) { 32 | swapRouter = _swapRouter; 33 | _approveAssets(_assets); 34 | } 35 | 36 | /** 37 | * @notice Strategist swaps assets sitting in the contract of the `assetHolder`. 38 | * @param _fromAsset The token address of the asset being sold by the vault. 39 | * @param _toAsset The token address of the asset being purchased by the vault. 40 | * @param _fromAssetAmount The amount of assets being sold by the vault. 41 | * @param _minToAssetAmount The minimum amount of assets to be purchased. 42 | * @param _data RLP encoded executer address and bytes data. This is re-encoded tx.data from 1Inch swap API 43 | */ 44 | function swap( 45 | address _fromAsset, 46 | address _toAsset, 47 | uint256 _fromAssetAmount, 48 | uint256 _minToAssetAmount, 49 | uint256, 50 | bytes calldata _data 51 | ) external returns (uint256 toAssetAmount) { 52 | if (IERC20(_fromAsset).allowance(address(this), address(swapRouter)) < _fromAssetAmount) 53 | IERC20(_fromAsset).forceApprove(swapRouter, type(uint256).max); 54 | 55 | // Decode the function selector from the RLP encoded _data param 56 | bytes4 swapSelector = bytes4(_data[:4]); 57 | 58 | if (swapSelector == SWAP_SELECTOR) { 59 | // Decode the executer address and data from the RLP encoded _data param 60 | (, address executer, bytes memory executerData) = abi.decode( 61 | _data, 62 | (bytes4, address, bytes) 63 | ); 64 | SwapDescription memory swapDesc = SwapDescription({ 65 | srcToken: IERC20(_fromAsset), 66 | dstToken: IERC20(_toAsset), 67 | srcReceiver: payable(executer), 68 | dstReceiver: payable(msg.sender), 69 | amount: _fromAssetAmount, 70 | minReturnAmount: _minToAssetAmount, 71 | flags: 0 // 1st bit _PARTIAL_FILL, 2nd bit _REQUIRES_EXTRA_ETH, 3rd bit _SHOULD_CLAIM 72 | }); 73 | (toAssetAmount, ) = IOneInchRouterV6(swapRouter).swap( 74 | IAggregationExecutor(executer), 75 | swapDesc, 76 | executerData 77 | ); 78 | } else if (swapSelector == UNOSWAP_TO_SELECTOR) { 79 | // Need to get the Uniswap pools data from the _data param 80 | (, uint256 dex) = abi.decode(_data, (bytes4, uint256)); 81 | toAssetAmount = IOneInchRouterV6(swapRouter).unoswapTo( 82 | uint256(uint160(msg.sender)), 83 | uint256(uint160(_fromAsset)), 84 | _fromAssetAmount, 85 | _minToAssetAmount, 86 | dex 87 | ); 88 | } else if (swapSelector == UNOSWAP_TO_2_SELECTOR) { 89 | // Need to get the Uniswap pools data from the _data param 90 | (, uint256 dex, uint256 dex2) = abi.decode( 91 | _data, 92 | (bytes4, uint256, uint256) 93 | ); 94 | toAssetAmount = IOneInchRouterV6(swapRouter).unoswapTo2( 95 | uint256(uint160(msg.sender)), 96 | uint256(uint160(_fromAsset)), 97 | _fromAssetAmount, 98 | _minToAssetAmount, 99 | dex, 100 | dex2 101 | ); 102 | } else { 103 | revert UnsupportedSwapFunction(); 104 | } 105 | 106 | if (toAssetAmount < _minToAssetAmount) revert OutputLessThanMinAmount(); 107 | } 108 | 109 | /** 110 | * @notice Approve assets for swapping. 111 | * @param _assets Array of token addresses to approve. 112 | * @dev unlimited approval is used as no tokens sit in this contract outside a transaction. 113 | */ 114 | function approveAssets(address[] memory _assets) external { 115 | _approveAssets(_assets); 116 | } 117 | 118 | function _approveAssets(address[] memory _assets) internal { 119 | for (uint256 i = 0; i < _assets.length; ++i) { 120 | // Give the 1Inch router approval to transfer unlimited assets 121 | IERC20(_assets[i]).forceApprove(swapRouter, type(uint256).max); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/utils/SwapperOpenOcean.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import {OpenOceanSwapDescription, IOpenOceanCaller, IOpenOceanRouter} from "../interfaces/IOpenOcean.sol"; 6 | import {ISwapper} from "../interfaces/ISwapper.sol"; 7 | 8 | contract SwapperOpenOcean is ISwapper { 9 | using SafeERC20 for IERC20; 10 | 11 | /// @notice OpenOcean router contract to give allowance to perform swaps 12 | address public immutable swapRouter; 13 | 14 | error OutputLessThanMinAmount(); 15 | 16 | constructor(address _swapRouter, address[] memory _assets) { 17 | swapRouter = _swapRouter; 18 | _approveAssets(_assets); 19 | } 20 | 21 | /** 22 | * @inheritdoc ISwapper 23 | */ 24 | function swap( 25 | address _fromAsset, 26 | address _toAsset, 27 | uint256 _fromAssetAmount, 28 | uint256 _minToAssetAmount, 29 | uint256 _guaranteedAmount, 30 | bytes calldata _data 31 | ) external returns (uint256 toAssetAmount) { 32 | if (IERC20(_fromAsset).allowance(address(this), address(swapRouter)) < _fromAssetAmount) 33 | IERC20(_fromAsset).forceApprove(swapRouter, type(uint256).max); 34 | 35 | ( 36 | , 37 | address executor, 38 | IOpenOceanCaller.CallDescription[] memory calls 39 | ) = abi.decode( 40 | _data, 41 | (bytes4, address, IOpenOceanCaller.CallDescription[]) 42 | ); 43 | 44 | OpenOceanSwapDescription memory swapDesc = OpenOceanSwapDescription({ 45 | srcToken: IERC20(_fromAsset), 46 | dstToken: IERC20(_toAsset), 47 | srcReceiver: payable(executor), 48 | dstReceiver: payable(msg.sender), 49 | amount: _fromAssetAmount, 50 | minReturnAmount: _minToAssetAmount, 51 | guaranteedAmount: _guaranteedAmount, 52 | flags: 2, 53 | referrer: msg.sender, 54 | permit: hex"" 55 | }); 56 | 57 | toAssetAmount = IOpenOceanRouter(swapRouter).swap( 58 | IOpenOceanCaller(executor), 59 | swapDesc, 60 | calls 61 | ); 62 | 63 | if (toAssetAmount < _minToAssetAmount) revert OutputLessThanMinAmount(); 64 | } 65 | 66 | /** 67 | * @notice Approve assets for swapping. 68 | * @param _assets Array of token addresses to approve. 69 | * @dev unlimited approval is used as no tokens sit in this contract outside a transaction. 70 | */ 71 | function approveAssets(address[] memory _assets) external { 72 | _approveAssets(_assets); 73 | } 74 | 75 | function _approveAssets(address[] memory _assets) internal { 76 | for (uint256 i = 0; i < _assets.length; ++i) { 77 | // Give the 1Inch router approval to transfer unlimited assets 78 | IERC20(_assets[i]).forceApprove(swapRouter, type(uint256).max); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/test/.DS_Store -------------------------------------------------------------------------------- /test/AaveAdapter/AaveAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {IPool} from "@aave/interfaces/IPool.sol"; 6 | import {IPoolDataProvider} from "@aave/interfaces/IPoolDataProvider.sol"; 7 | import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | import {MockERC20} from "../../src/mocks/MockERC20.sol"; 9 | import {Utils, ChainConfig} from "../Utils.sol"; 10 | import {IEtherFiCashAaveV3Adapter, EtherFiCashAaveV3Adapter} from "../../src/adapters/aave-v3/EtherFiCashAaveV3Adapter.sol"; 11 | import {MockAaveAdapter} from "../../src/mocks/MockAaveAdapter.sol"; 12 | 13 | contract AaveAdapterTest is Utils { 14 | using SafeERC20 for IERC20; 15 | 16 | address owner = makeAddr("owner"); 17 | IEtherFiCashAaveV3Adapter aaveV3Adapter; 18 | 19 | IPool aavePool; 20 | IPoolDataProvider aaveV3PoolDataProvider; 21 | // Interest rate mode -> Stable: 1, variable: 2 22 | uint256 interestRateMode = 2; 23 | uint16 aaveReferralCode = 0; 24 | 25 | IERC20 weth; 26 | IERC20 usdc; 27 | string chainId; 28 | 29 | function setUp() public { 30 | chainId = vm.envString("TEST_CHAIN"); 31 | 32 | vm.startPrank(owner); 33 | 34 | if (!isFork(chainId)) { 35 | emit log_named_string("Testing on ChainID", chainId); 36 | 37 | usdc = IERC20(address(new MockERC20("USDC", "USDC", 6))); 38 | weth = IERC20(address(new MockERC20("WETH", "WETH", 18))); 39 | aaveV3Adapter = IEtherFiCashAaveV3Adapter(new MockAaveAdapter()); 40 | } else { 41 | emit log_named_string("Testing on ChainID", chainId); 42 | 43 | ChainConfig memory chainConfig = getChainConfig(chainId); 44 | vm.createSelectFork(chainConfig.rpc); 45 | 46 | usdc = IERC20(chainConfig.usdc); 47 | weth = IERC20(chainConfig.weth); 48 | 49 | aavePool = IPool(chainConfig.aaveV3Pool); 50 | aaveV3PoolDataProvider = IPoolDataProvider( 51 | chainConfig.aaveV3PoolDataProvider 52 | ); 53 | 54 | aaveV3Adapter = IEtherFiCashAaveV3Adapter( 55 | new EtherFiCashAaveV3Adapter( 56 | address(aavePool), 57 | address(aaveV3PoolDataProvider), 58 | aaveReferralCode, 59 | interestRateMode 60 | ) 61 | ); 62 | 63 | deal(address(usdc), owner, 1 ether); 64 | deal(address(weth), owner, 1000 ether); 65 | } 66 | 67 | vm.stopPrank(); 68 | } 69 | 70 | function test_AaveFullFlow() public { 71 | test_FullFlow(); 72 | } 73 | 74 | function test_AaveProcess() public { 75 | test_Borrow(); 76 | } 77 | 78 | function test_Supply() internal returns (uint256) { 79 | vm.startPrank(owner); 80 | 81 | uint256 amount = 0.001 ether; 82 | weth.safeTransfer(address(aaveV3Adapter), amount); 83 | aaveV3Adapter.supply(address(weth), amount); 84 | 85 | uint256 collateralBalance = aaveV3Adapter.getCollateralBalance( 86 | address(aaveV3Adapter), 87 | address(weth) 88 | ); 89 | assertEq(collateralBalance, amount); 90 | 91 | vm.stopPrank(); 92 | 93 | return amount; 94 | } 95 | 96 | function test_Borrow() internal returns (uint256, uint256) { 97 | uint256 supplyAmt = test_Supply(); 98 | vm.startPrank(owner); 99 | 100 | vm.roll(block.number + 10); 101 | uint256 usdcBalBefore = usdc.balanceOf(address(aaveV3Adapter)); 102 | 103 | uint256 borrowAmt = 0.1e6; 104 | 105 | if (!isFork(chainId)) { 106 | usdc.safeTransfer(address(aaveV3Adapter), borrowAmt); 107 | } 108 | 109 | aaveV3Adapter.borrow(address(usdc), borrowAmt); 110 | 111 | uint256 usdcBalAfter = usdc.balanceOf(address(aaveV3Adapter)); 112 | 113 | uint256 debt = aaveV3Adapter.getDebt( 114 | address(aaveV3Adapter), 115 | address(usdc) 116 | ); 117 | assertApproxEqAbs(debt, borrowAmt, 1); 118 | assertApproxEqAbs(usdcBalAfter - usdcBalBefore, borrowAmt, 1); 119 | 120 | vm.stopPrank(); 121 | 122 | return (supplyAmt, borrowAmt); 123 | } 124 | 125 | function test_Repay() internal returns (uint256, uint256, uint256) { 126 | (uint256 supplyAmt, uint256 borrowAmt) = test_Borrow(); 127 | vm.startPrank(owner); 128 | 129 | vm.roll(block.number + 10); 130 | uint256 repayAmt = aaveV3Adapter.getDebt( 131 | address(aaveV3Adapter), 132 | address(usdc) 133 | ); 134 | 135 | usdc.safeTransfer(address(aaveV3Adapter), borrowAmt); 136 | aaveV3Adapter.repay(address(usdc), repayAmt); 137 | 138 | uint256 finalDebt = aaveV3Adapter.getDebt( 139 | address(aaveV3Adapter), 140 | address(usdc) 141 | ); 142 | 143 | assertEq(finalDebt, 0); 144 | 145 | vm.stopPrank(); 146 | 147 | return (supplyAmt, borrowAmt, repayAmt); 148 | } 149 | 150 | function test_FullFlow() internal { 151 | test_Repay(); 152 | vm.startPrank(owner); 153 | 154 | vm.roll(block.number + 10); 155 | uint256 balBefore = weth.balanceOf(address(aaveV3Adapter)); 156 | 157 | uint256 withdrawAmt = aaveV3Adapter.getCollateralBalance( 158 | address(aaveV3Adapter), 159 | address(weth) 160 | ); 161 | 162 | if (!isFork(chainId)) { 163 | weth.safeTransfer(address(aaveV3Adapter), withdrawAmt); 164 | } 165 | 166 | aaveV3Adapter.withdraw(address(weth), withdrawAmt); 167 | 168 | uint256 balAfter = weth.balanceOf(address(aaveV3Adapter)); 169 | assertEq(balAfter - balBefore, withdrawAmt); 170 | vm.stopPrank(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/CashWrappedToken/CashWrappedTokenFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test, stdError} from "forge-std/Test.sol"; 5 | import {CashTokenWrapperFactory, CashWrappedERC20} from "../../src/cash-wrapper-token/CashTokenWrapperFactory.sol"; 6 | import {MockERC20} from "../../src/mocks/MockERC20.sol"; 7 | import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 8 | 9 | error OwnableUnauthorizedAccount(address account); 10 | 11 | contract CashWrappedERC20V2 is CashWrappedERC20 { 12 | function version() public pure returns (uint256) { 13 | return 2; 14 | } 15 | } 16 | 17 | contract CashWrappedTokenFactoryTest is Test { 18 | address owner = makeAddr("owner"); 19 | address notOwner = makeAddr("notOwner"); 20 | 21 | CashTokenWrapperFactory tokenFactory; 22 | CashWrappedERC20 impl; 23 | MockERC20 weETH; 24 | MockERC20 usdc; 25 | CashWrappedERC20 wweETH; 26 | CashWrappedERC20 wUsdc; 27 | 28 | function setUp() public { 29 | vm.startPrank(owner); 30 | weETH = new MockERC20("Wrapped eETH", "weETH", 18); 31 | usdc = new MockERC20("USDC", "USDC", 6); 32 | impl = new CashWrappedERC20(); 33 | tokenFactory = new CashTokenWrapperFactory(address(impl), owner); 34 | wweETH = CashWrappedERC20(tokenFactory.deployWrapper(address(weETH))); 35 | wUsdc = CashWrappedERC20(tokenFactory.deployWrapper(address(usdc))); 36 | vm.stopPrank(); 37 | } 38 | 39 | function test_DeployCashWrappedTokenFactory() public view { 40 | assertEq(wweETH.name(), "eCash Wrapped eETH"); 41 | assertEq(wweETH.symbol(), "ecweETH"); 42 | assertEq(wweETH.decimals(), weETH.decimals()); 43 | 44 | assertEq(wUsdc.name(), "eCash USDC"); 45 | assertEq(wUsdc.symbol(), "ecUSDC"); 46 | assertEq(wUsdc.decimals(), usdc.decimals()); 47 | } 48 | 49 | 50 | function test_Upgrade() public { 51 | CashWrappedERC20V2 implV2 = new CashWrappedERC20V2(); 52 | 53 | vm.prank(notOwner); 54 | vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, notOwner)); 55 | tokenFactory.upgradeTo(address(implV2)); 56 | 57 | vm.prank(owner); 58 | tokenFactory.upgradeTo(address(implV2)); 59 | 60 | assertEq(CashWrappedERC20V2(address(wUsdc)).version(), 2); 61 | assertEq(CashWrappedERC20V2(address(wweETH)).version(), 2); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/DebtManager/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/test/DebtManager/.DS_Store -------------------------------------------------------------------------------- /test/DebtManager/Deploy.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Setup, IL2DebtManager} from "../Setup.t.sol"; 5 | import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; 6 | 7 | contract DebtManagerDeployTest is Setup { 8 | function test_Deploy() public { 9 | assertEq( 10 | address(debtManager.cashDataProvider()), 11 | address(cashDataProvider) 12 | ); 13 | 14 | assertEq( 15 | IAccessControl(address(debtManager)).hasRole(DEFAULT_ADMIN_ROLE, owner), 16 | true 17 | ); 18 | assertEq(IAccessControl(address(debtManager)).hasRole(ADMIN_ROLE, owner), true); 19 | assertEq( 20 | IAccessControl(address(debtManager)).hasRole(DEFAULT_ADMIN_ROLE, notOwner), 21 | false 22 | ); 23 | assertEq( 24 | IAccessControl(address(debtManager)).hasRole(ADMIN_ROLE, notOwner), 25 | false 26 | ); 27 | 28 | IL2DebtManager.CollateralTokenConfig memory config = debtManager.collateralTokenConfig(address(weETH)); 29 | assertEq(config.ltv, ltv); 30 | assertEq(config.liquidationThreshold, liquidationThreshold); 31 | assertEq(config.liquidationBonus, liquidationBonus); 32 | assertEq(debtManager.borrowApyPerSecond(address(usdc)), borrowApyPerSecond); 33 | assertEq(debtManager.borrowTokenMinShares(address(usdc)), minShares); 34 | 35 | assertEq(debtManager.getCollateralTokens().length, 2); 36 | assertEq(debtManager.getCollateralTokens()[0], address(weETH)); 37 | assertEq(debtManager.getCollateralTokens()[1], address(usdc)); 38 | 39 | assertEq(debtManager.getBorrowTokens().length, 1); 40 | assertEq(debtManager.getBorrowTokens()[0], address(usdc)); 41 | 42 | ( 43 | IL2DebtManager.TokenData[] memory borrowings, 44 | uint256 totalBorrowingsInUsd, 45 | IL2DebtManager.TokenData[] memory totalLiquidStableAmounts 46 | ) = debtManager.getCurrentState(); 47 | 48 | assertEq(borrowings.length, 0); 49 | assertEq(totalBorrowingsInUsd, 0); 50 | assertEq(totalLiquidStableAmounts.length, 0); 51 | 52 | ( 53 | IL2DebtManager.TokenData[] memory totalUserCollaterals, 54 | uint256 totalUserCollateralInUsd, 55 | IL2DebtManager.TokenData[] memory userBorrowings, 56 | uint256 totalUserBorrowings 57 | ) = debtManager.getUserCurrentState(address(aliceSafe)); 58 | assertEq(totalUserCollaterals.length, 0); 59 | assertEq(totalUserCollateralInUsd, 0); 60 | 61 | assertEq(userBorrowings.length, 0); 62 | assertEq(totalUserBorrowings, 0); 63 | 64 | ( 65 | IL2DebtManager.TokenData[] memory supplierBalances, 66 | uint256 totalSupplierBalance 67 | ) = debtManager.supplierBalance(alice); 68 | assertEq(supplierBalances.length, 0); 69 | assertEq(totalSupplierBalance, 0); 70 | 71 | assertEq(debtManager.supplierBalance(alice, address(usdc)), 0); 72 | assertEq(debtManager.totalSupplies(address(usdc)), 0); 73 | 74 | ( 75 | IL2DebtManager.TokenData[] memory suppliedTokenBalances, 76 | uint256 totalSuppliedInUsd 77 | ) = debtManager.totalSupplies(); 78 | assertEq(suppliedTokenBalances.length, 0); 79 | assertEq(totalSuppliedInUsd, 0); 80 | 81 | address unsupportedToken = makeAddr("unsupportedToken"); 82 | vm.expectRevert(IL2DebtManager.UnsupportedCollateralToken.selector); 83 | debtManager.convertCollateralTokenToUsd(address(unsupportedToken), 1); 84 | 85 | vm.expectRevert(IL2DebtManager.UnsupportedCollateralToken.selector); 86 | debtManager.convertUsdToCollateralToken(address(unsupportedToken), 1); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/ForkTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test, stdError} from "forge-std/Test.sol"; 5 | import {Utils, ChainConfig} from "../script/user-safe/Utils.sol"; 6 | import {DebtManagerCore} from "../src/debt-manager/DebtManagerCore.sol"; 7 | import {stdJson} from "forge-std/StdJson.sol"; 8 | 9 | contract ForkTest is Utils { 10 | using stdJson for string; 11 | 12 | address user = 0x2F9e38E716AD75B6f8005C65BD727183137393F1; 13 | address borrowToken = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; 14 | uint256 withdrawAmt = 10000000; 15 | DebtManagerCore debtManager; 16 | 17 | function setUp() public { 18 | vm.createSelectFork("https://1rpc.io/scroll"); 19 | 20 | string memory deployments = readDeploymentFile(); 21 | debtManager = DebtManagerCore(stdJson.readAddress( 22 | deployments, 23 | string.concat(".", "addresses", ".", "debtManagerProxy") 24 | ) 25 | ); 26 | } 27 | 28 | // function test_Withdraw() public { 29 | // vm.prank(user); 30 | // debtManager.withdrawBorrowToken(borrowToken, withdrawAmt); 31 | // } 32 | } 33 | -------------------------------------------------------------------------------- /test/Gnosis/SetUsdcConfig.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {IL2DebtManager} from "../../src/interfaces/IL2DebtManager.sol"; 6 | import {GnosisHelpers} from "../../utils/GnosisHelpers.sol"; 7 | 8 | contract SetUsdcConfig is Test, GnosisHelpers { 9 | address usdc = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; 10 | address debtManager = 0x8f9d2Cd33551CE06dD0564Ba147513F715c2F4a0; 11 | address owner = 0xA6cf33124cb342D1c604cAC87986B965F428AAC4; 12 | 13 | function setUp() external { 14 | vm.createSelectFork("https://rpc.scroll.io"); 15 | } 16 | 17 | function test_SetConfig() public { 18 | IL2DebtManager.CollateralTokenConfig memory collateralTokenConfig; 19 | collateralTokenConfig.ltv = 90e18; 20 | collateralTokenConfig.liquidationThreshold = 95e18; 21 | collateralTokenConfig.liquidationBonus = 1e18; 22 | 23 | string memory gnosisTx = _getGnosisHeader(vm.toString(block.chainid)); 24 | string memory setCollateralConfig = iToHex(abi.encodeWithSelector(IL2DebtManager.setCollateralTokenConfig.selector, usdc, collateralTokenConfig)); 25 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(debtManager), setCollateralConfig, false))); 26 | 27 | 28 | string memory setBorrowApyZero = iToHex(abi.encodeWithSelector(IL2DebtManager.setBorrowApy.selector, usdc, 1)); 29 | gnosisTx = string(abi.encodePacked(gnosisTx, _getGnosisTransaction(addressToHex(debtManager), setBorrowApyZero, true))); 30 | 31 | vm.createDir("./output", true); 32 | string memory path = "./output/SetUsdcConfig.json"; 33 | 34 | vm.writeFile(path, gnosisTx); 35 | 36 | executeGnosisTransactionBundle(path, owner); 37 | } 38 | } -------------------------------------------------------------------------------- /test/IntegrationTest/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/test/IntegrationTest/.DS_Store -------------------------------------------------------------------------------- /test/Swapper/Swapper1Inch.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import {Utils, ChainConfig} from "../Utils.sol"; 6 | import {Swapper1InchV6} from "../../src/utils/Swapper1InchV6.sol"; 7 | 8 | contract Swapper1InchV6Test is Utils { 9 | Swapper1InchV6 swapper1InchV6; 10 | string chainId; 11 | ERC20 usdt; 12 | ERC20 usdc; 13 | address alice = makeAddr("alice"); 14 | 15 | function setUp() public { 16 | chainId = vm.envString("TEST_CHAIN"); 17 | 18 | if (isFork(chainId)) { 19 | ChainConfig memory chainConfig = getChainConfig(chainId); 20 | vm.createSelectFork(chainConfig.rpc); 21 | 22 | usdt = ERC20(chainConfig.usdt); 23 | usdc = ERC20(chainConfig.usdc); 24 | address router = chainConfig.swapRouter1InchV6; 25 | address[] memory assets = new address[](1); 26 | assets[0] = address(usdt); 27 | 28 | swapper1InchV6 = new Swapper1InchV6(router, assets); 29 | } 30 | } 31 | 32 | function test_Swap() public { 33 | if (!isFork(chainId) || isScroll(chainId)) return; 34 | 35 | vm.startPrank(alice); 36 | deal(address(usdc), alice, 1 ether); 37 | deal(address(usdt), alice, 1 ether); 38 | uint256 aliceUsdcBalBefore = usdc.balanceOf(alice); 39 | usdt.transfer(address(swapper1InchV6), 1000e6); 40 | 41 | bytes memory swapData = getQuoteOneInch( 42 | chainId, 43 | address(swapper1InchV6), 44 | address(alice), 45 | address(usdt), 46 | address(usdc), 47 | 1000e6, 48 | usdt.decimals() 49 | ); 50 | 51 | swapper1InchV6.swap( 52 | address(usdt), 53 | address(usdc), 54 | 1000e6, 55 | 1, 56 | 1, 57 | swapData 58 | ); 59 | 60 | uint256 aliceUsdcBalAfter = usdc.balanceOf(alice); 61 | assertGt(aliceUsdcBalAfter - aliceUsdcBalBefore, 0); 62 | 63 | vm.stopPrank(); 64 | } 65 | } -------------------------------------------------------------------------------- /test/Swapper/SwapperOpenOcean.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import {Utils, ChainConfig} from "../Utils.sol"; 6 | import {SwapperOpenOcean} from "../../src/utils/SwapperOpenOcean.sol"; 7 | 8 | contract SwapperOpenOceanTest is Utils { 9 | SwapperOpenOcean swapperOpenOcean; 10 | string chainId; 11 | ERC20 usdt; 12 | ERC20 usdc; 13 | address alice = makeAddr("alice"); 14 | 15 | function setUp() public { 16 | chainId = vm.envString("TEST_CHAIN"); 17 | 18 | if (isFork(chainId)) { 19 | ChainConfig memory chainConfig = getChainConfig(chainId); 20 | vm.createSelectFork(chainConfig.rpc); 21 | 22 | usdt = ERC20(chainConfig.usdt); 23 | usdc = ERC20(chainConfig.usdc); 24 | address router = chainConfig.swapRouterOpenOcean; 25 | address[] memory assets = new address[](1); 26 | assets[0] = address(usdt); 27 | 28 | swapperOpenOcean = new SwapperOpenOcean(router, assets); 29 | } 30 | } 31 | 32 | function test_Swap() public { 33 | if (!isFork(chainId)) return; 34 | 35 | vm.startPrank(alice); 36 | deal(address(usdc), alice, 1 ether); 37 | deal(address(usdt), alice, 1 ether); 38 | uint256 aliceUsdcBalBefore = usdc.balanceOf(alice); 39 | usdt.transfer(address(swapperOpenOcean), 1000e6); 40 | 41 | bytes memory swapData = getQuoteOpenOcean( 42 | chainId, 43 | address(swapperOpenOcean), 44 | address(alice), 45 | address(usdt), 46 | address(usdc), 47 | 1000e6, 48 | usdt.decimals() 49 | ); 50 | 51 | swapperOpenOcean.swap( 52 | address(usdt), 53 | address(usdc), 54 | 1000e6, 55 | 1, 56 | 1, 57 | swapData 58 | ); 59 | 60 | uint256 aliceUsdcBalAfter = usdc.balanceOf(alice); 61 | assertGt(aliceUsdcBalAfter - aliceUsdcBalBefore, 0); 62 | 63 | vm.stopPrank(); 64 | } 65 | } -------------------------------------------------------------------------------- /test/UserSafe/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etherfi-protocol/cash-contracts/c270e3b0f1606ecfaf6ba958068cb920b367f7f6/test/UserSafe/.DS_Store -------------------------------------------------------------------------------- /test/UserSafe/Deploy.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, OwnerLib, SpendingLimit, UserSafeCore} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {Setup} from "../Setup.t.sol"; 6 | import {CREATE3} from "solady/utils/CREATE3.sol"; 7 | 8 | contract UserSafeDeployTest is Setup { 9 | address bob = makeAddr("bob"); 10 | bytes bobBytes = abi.encode(bob); 11 | 12 | function test_Deploy() public view { 13 | assertEq(aliceSafe.owner().ethAddr, alice); 14 | assertEq(aliceSafe.recoverySigners()[0].ethAddr, address(0)); 15 | assertEq(aliceSafe.recoverySigners()[1].ethAddr, etherFiRecoverySigner); 16 | assertEq(aliceSafe.recoverySigners()[2].ethAddr, thirdPartyRecoverySigner); 17 | 18 | SpendingLimit memory spendingLimit = aliceSafe.applicableSpendingLimit(); 19 | assertEq(spendingLimit.dailyLimit, defaultDailySpendingLimit); 20 | assertEq(spendingLimit.monthlyLimit, defaultMonthlySpendingLimit); 21 | assertEq(spendingLimit.spentToday, 0); 22 | assertEq(spendingLimit.spentThisMonth, 0); 23 | assertEq(spendingLimit.newDailyLimit, 0); 24 | assertEq(spendingLimit.newMonthlyLimit, 0); 25 | assertEq(spendingLimit.dailyLimitChangeActivationTime, 0); 26 | assertEq(spendingLimit.monthlyLimitChangeActivationTime, 0); 27 | assertEq(spendingLimit.timezoneOffset, timezoneOffset); 28 | } 29 | 30 | function test_DeployAUserSafe() public { 31 | bytes memory salt = abi.encode("safe", block.timestamp); 32 | 33 | bytes memory initData = abi.encodeWithSelector( 34 | UserSafeCore.initialize.selector, 35 | bobBytes, 36 | defaultDailySpendingLimit, 37 | defaultMonthlySpendingLimit, 38 | timezoneOffset 39 | ); 40 | 41 | address deterministicAddress = factory.getUserSafeAddress(salt, initData); 42 | vm.prank(owner); 43 | address safe = factory.createUserSafe(salt, initData); 44 | assertEq(deterministicAddress, safe); 45 | } 46 | 47 | function test_CannotDeployTwoUserSafesAtTheSameAddress() public { 48 | vm.startPrank(owner); 49 | bytes memory salt = abi.encode("safe", block.timestamp); 50 | 51 | bytes memory initData = abi.encodeWithSelector( 52 | UserSafeCore.initialize.selector, 53 | bobBytes, 54 | defaultDailySpendingLimit, 55 | defaultMonthlySpendingLimit, 56 | timezoneOffset 57 | ); 58 | 59 | address deterministicAddress = factory.getUserSafeAddress(salt, initData); 60 | address safe = factory.createUserSafe(salt, initData); 61 | assertEq(deterministicAddress, safe); 62 | 63 | vm.expectRevert(CREATE3.DeploymentFailed.selector); 64 | factory.createUserSafe(salt, initData); 65 | vm.stopPrank(); 66 | } 67 | } -------------------------------------------------------------------------------- /test/UserSafe/Mode.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {UserSafeLib, SpendingLimit, SpendingLimitLib, IUserSafe} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {Setup} from "../Setup.t.sol"; 7 | 8 | contract UserSafeModeTest is Setup { 9 | using MessageHashUtils for bytes32; 10 | 11 | function test_InitialModeIsDebit() public view { 12 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 13 | } 14 | 15 | function test_SwitchToCreditModeIncursDelay() public { 16 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 17 | _setMode(IUserSafe.Mode.Credit, bytes4(0)); 18 | 19 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 20 | assertEq(aliceSafe.incomingCreditModeStartTime(), block.timestamp + delay); 21 | 22 | vm.warp(block.timestamp + delay); 23 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 24 | 25 | vm.warp(block.timestamp + 1); 26 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Credit)); 27 | } 28 | 29 | function test_SwitchToDebitModeDoesNotIncursDelay() public { 30 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 31 | _setMode(IUserSafe.Mode.Credit, bytes4(0)); 32 | 33 | vm.warp(aliceSafe.incomingCreditModeStartTime() + 1); 34 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Credit)); 35 | 36 | _setMode(IUserSafe.Mode.Debit, bytes4(0)); 37 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 38 | assertEq(aliceSafe.incomingCreditModeStartTime(), 0); 39 | } 40 | 41 | function test_CannotSetTheSameMode() public { 42 | _setMode(IUserSafe.Mode.Debit, IUserSafe.ModeAlreadySet.selector); 43 | 44 | _setMode(IUserSafe.Mode.Credit, bytes4(0)); 45 | vm.warp(aliceSafe.incomingCreditModeStartTime() + 1); 46 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Credit)); 47 | _setMode(IUserSafe.Mode.Credit, IUserSafe.ModeAlreadySet.selector); 48 | } 49 | 50 | function test_CanSetDebitModeIfBorrowIsNotRepaid() public { 51 | uint256 weETHCollateralAmount = 1 ether; 52 | deal(address(weETH), address(aliceSafe), weETHCollateralAmount); 53 | deal(address(usdc), address(debtManager), 1 ether); 54 | 55 | uint256 totalMaxBorrow = debtManager.getMaxBorrowAmount(address(aliceSafe), true); 56 | uint256 spendDebitAmount = 10e6; 57 | uint256 borrowAmt = totalMaxBorrow - spendDebitAmount; 58 | 59 | _setMode(IUserSafe.Mode.Credit, bytes4(0)); 60 | vm.warp(aliceSafe.incomingCreditModeStartTime() + 1); 61 | 62 | vm.prank(etherFiWallet); 63 | aliceSafe.spend(txId, address(usdc), borrowAmt); 64 | 65 | _setMode(IUserSafe.Mode.Debit, bytes4(0)); 66 | 67 | assertEq(uint8(aliceSafe.mode()), uint8(IUserSafe.Mode.Debit)); 68 | } 69 | 70 | function _setMode(IUserSafe.Mode mode, bytes4 errorSelector) internal { 71 | uint256 nonce = aliceSafe.nonce() + 1; 72 | bytes32 msgHash = keccak256( 73 | abi.encode( 74 | UserSafeLib.SET_MODE_METHOD, 75 | block.chainid, 76 | address(aliceSafe), 77 | nonce, 78 | mode 79 | ) 80 | ); 81 | 82 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 83 | alicePk, 84 | msgHash.toEthSignedMessageHash() 85 | ); 86 | 87 | bytes memory signature = abi.encodePacked(r, s, v); 88 | if (errorSelector != bytes4(0)) vm.expectRevert(errorSelector); 89 | aliceSafe.setMode(mode, signature); 90 | } 91 | } -------------------------------------------------------------------------------- /test/UserSafe/Owner.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, UserSafeEventEmitter, OwnerLib, UserSafeLib} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {Setup} from "../Setup.t.sol"; 7 | 8 | contract UserSafeOwnerTest is Setup { 9 | using MessageHashUtils for bytes32; 10 | using OwnerLib for bytes; 11 | 12 | function test_CanSetEthereumAddrAsOwner() public { 13 | address newOwner = makeAddr("newOwner"); 14 | bytes memory newOwnerBytes = abi.encode(newOwner); 15 | 16 | bytes memory signature = _signSetOwner(newOwnerBytes); 17 | 18 | vm.prank(alice); 19 | vm.expectEmit(true, true, true, true); 20 | emit UserSafeEventEmitter.OwnerSet(address(aliceSafe), aliceSafe.owner(), newOwnerBytes.getOwnerObject()); 21 | aliceSafe.setOwner(newOwnerBytes, signature); 22 | 23 | assertEq(aliceSafe.owner().ethAddr, newOwner); 24 | assertEq(aliceSafe.owner().x, 0); 25 | assertEq(aliceSafe.owner().y, 0); 26 | } 27 | 28 | function test_CanSetPasskeyAsOwner() public { 29 | uint256 x = 1; 30 | uint256 y = 2; 31 | 32 | bytes memory newOwnerBytes = abi.encode(x, y); 33 | bytes memory signature = _signSetOwner(newOwnerBytes); 34 | 35 | vm.prank(alice); 36 | vm.expectEmit(true, true, true, true); 37 | emit UserSafeEventEmitter.OwnerSet(address(aliceSafe), aliceSafe.owner(), newOwnerBytes.getOwnerObject()); 38 | aliceSafe.setOwner(newOwnerBytes, signature); 39 | 40 | assertEq(aliceSafe.owner().ethAddr, address(0)); 41 | assertEq(aliceSafe.owner().x, x); 42 | assertEq(aliceSafe.owner().y, y); 43 | } 44 | 45 | function _signSetOwner( 46 | bytes memory ownerBytes 47 | ) internal view returns (bytes memory) { 48 | uint256 nonce = aliceSafe.nonce() + 1; 49 | bytes32 msgHash = keccak256( 50 | abi.encode( 51 | UserSafeLib.SET_OWNER_METHOD, 52 | block.chainid, 53 | address(aliceSafe), 54 | nonce, 55 | ownerBytes 56 | ) 57 | ); 58 | 59 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 60 | alicePk, 61 | msgHash.toEthSignedMessageHash() 62 | ); 63 | 64 | bytes memory signature = abi.encodePacked(r, s, v); 65 | return signature; 66 | } 67 | } -------------------------------------------------------------------------------- /test/UserSafe/PendingCashback.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, UserSafeEventEmitter, OwnerLib, UserSafeLib, SpendingLimitLib, UserSafeStorage} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {Setup, ERC20, MockPriceProvider} from "../Setup.t.sol"; 7 | import {ICashDataProvider} from "../../src/interfaces/ICashDataProvider.sol"; 8 | import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 9 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 10 | 11 | contract UserSafePendingCashbackTest is Setup { 12 | using MessageHashUtils for bytes32; 13 | 14 | uint256 pepeCashbackPercentage = 200; 15 | uint256 wojakCashbackPercentage = 300; 16 | uint256 chadCashbackPercentage = 400; 17 | uint256 whaleCashbackPercentage = 500; 18 | 19 | function setUp() public override { 20 | super.setUp(); 21 | 22 | vm.startPrank(owner); 23 | 24 | if (!isFork(chainId)) MockPriceProvider(address(priceProvider)).setStableToken(address(scr)); 25 | 26 | ICashDataProvider.UserSafeTiers[] memory userSafeTiers = new ICashDataProvider.UserSafeTiers[](4); 27 | userSafeTiers[0] = ICashDataProvider.UserSafeTiers.Pepe; 28 | userSafeTiers[1] = ICashDataProvider.UserSafeTiers.Wojak; 29 | userSafeTiers[2] = ICashDataProvider.UserSafeTiers.Chad; 30 | userSafeTiers[3] = ICashDataProvider.UserSafeTiers.Whale; 31 | 32 | uint256[] memory cashbackPercentages = new uint256[](4); 33 | cashbackPercentages[0] = pepeCashbackPercentage; 34 | cashbackPercentages[1] = wojakCashbackPercentage; 35 | cashbackPercentages[2] = chadCashbackPercentage; 36 | cashbackPercentages[3] = whaleCashbackPercentage; 37 | 38 | cashDataProvider.setTierCashbackPercentage(userSafeTiers, cashbackPercentages); 39 | 40 | vm.stopPrank(); 41 | } 42 | 43 | function test_CanGetPendingCashback() public { 44 | uint256 spendAmt = 100e6; 45 | deal(address(usdc), address(aliceSafe), 100e6); 46 | 47 | // alice is pepe, so cashback is 2% -> 2 USDC in scroll tokens 48 | uint256 cashbackInUsdc = (spendAmt * pepeCashbackPercentage) / HUNDRED_PERCENT_IN_BPS; 49 | uint256 cashbackInScroll = (cashbackInUsdc * 10 ** scr.decimals()) / priceProvider.price(address(scr)); 50 | 51 | assertEq(aliceSafe.pendingCashback(), 0); 52 | 53 | vm.prank(etherFiWallet); 54 | vm.expectEmit(true, true, true, true); 55 | emit UserSafeEventEmitter.Cashback(address(aliceSafe), spendAmt, address(scr), cashbackInScroll, cashbackInUsdc, false); 56 | aliceSafe.spend(txId, address(usdc), spendAmt); 57 | 58 | assertEq(aliceSafe.pendingCashback(), cashbackInUsdc); 59 | assertEq(aliceSafe.totalCashbackEarnedInUsd(), cashbackInUsdc); 60 | } 61 | 62 | function test_RetrievePendingCashback() public { 63 | uint256 spendAmt = 100e6; 64 | deal(address(usdc), address(aliceSafe), 100e6); 65 | 66 | // alice is pepe, so cashback is 2% -> 2 USDC in scroll tokens 67 | uint256 cashbackInUsdc = (spendAmt * pepeCashbackPercentage) / HUNDRED_PERCENT_IN_BPS; 68 | uint256 cashbackInScroll = (cashbackInUsdc * 10 ** scr.decimals()) / priceProvider.price(address(scr)); 69 | 70 | assertEq(aliceSafe.pendingCashback(), 0); 71 | 72 | vm.prank(etherFiWallet); 73 | vm.expectEmit(true, true, true, true); 74 | emit UserSafeEventEmitter.Cashback(address(aliceSafe), spendAmt, address(scr), cashbackInScroll, cashbackInUsdc, false); 75 | aliceSafe.spend(txId, address(usdc), spendAmt); 76 | 77 | assertEq(aliceSafe.pendingCashback(), cashbackInUsdc); 78 | assertEq(aliceSafe.totalCashbackEarnedInUsd(), cashbackInUsdc); 79 | 80 | deal(address(scr), address(cashbackDispatcher), cashbackInScroll); 81 | 82 | uint256 aliceSafeScrBalBefore = scr.balanceOf(address(aliceSafe)); 83 | 84 | vm.expectEmit(true, true, true, true); 85 | emit UserSafeEventEmitter.PendingCashbackCleared(address(aliceSafe), address(scr), cashbackInScroll, cashbackInUsdc); 86 | aliceSafe.retrievePendingCashback(); 87 | 88 | uint256 aliceSafeScrBalAfter = scr.balanceOf(address(aliceSafe)); 89 | assertApproxEqAbs(aliceSafeScrBalAfter - aliceSafeScrBalBefore, cashbackInScroll, 1000); 90 | assertEq(aliceSafe.totalCashbackEarnedInUsd(), cashbackInUsdc); 91 | } 92 | 93 | function test_RetrievePendingCashbackWhenNoPendingCashbackJustReturns() public { 94 | assertEq(aliceSafe.pendingCashback(), 0); 95 | 96 | uint256 aliceSafeScrBalBefore = scr.balanceOf(address(aliceSafe)); 97 | aliceSafe.retrievePendingCashback(); 98 | uint256 aliceSafeScrBalAfter = scr.balanceOf(address(aliceSafe)); 99 | assertEq(aliceSafeScrBalAfter, aliceSafeScrBalBefore); 100 | } 101 | 102 | function test_RetrievePendingCashbackWhenBalNotAvailableJustReturns() public { 103 | uint256 spendAmt = 100e6; 104 | deal(address(usdc), address(aliceSafe), 100e6); 105 | 106 | // alice is pepe, so cashback is 2% -> 2 USDC in scroll tokens 107 | uint256 cashbackInUsdc = (spendAmt * pepeCashbackPercentage) / HUNDRED_PERCENT_IN_BPS; 108 | uint256 cashbackInScroll = (cashbackInUsdc * 10 ** scr.decimals()) / priceProvider.price(address(scr)); 109 | 110 | assertEq(aliceSafe.pendingCashback(), 0); 111 | 112 | vm.prank(etherFiWallet); 113 | vm.expectEmit(true, true, true, true); 114 | emit UserSafeEventEmitter.Cashback(address(aliceSafe), spendAmt, address(scr), cashbackInScroll, cashbackInUsdc, false); 115 | aliceSafe.spend(txId, address(usdc), spendAmt); 116 | 117 | assertEq(aliceSafe.pendingCashback(), cashbackInUsdc); 118 | 119 | uint256 aliceSafeScrBalBefore = scr.balanceOf(address(aliceSafe)); 120 | aliceSafe.retrievePendingCashback(); 121 | 122 | uint256 aliceSafeScrBalAfter = scr.balanceOf(address(aliceSafe)); 123 | assertEq(aliceSafeScrBalAfter, aliceSafeScrBalBefore); 124 | assertEq(aliceSafe.pendingCashback(), cashbackInUsdc); 125 | } 126 | } -------------------------------------------------------------------------------- /test/UserSafe/RecoveryUsingSafe.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, UserSafeEventEmitter, OwnerLib, UserSafeLib} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {Setup} from "../Setup.t.sol"; 7 | import {console} from "forge-std/console.sol"; 8 | 9 | contract RecoverUserSafe is Setup { 10 | using MessageHashUtils for bytes32; 11 | 12 | // rupert safe: 0x5BBa1D1b2820c56c798a831cCF7Ad39B70b13A01; 13 | // rupert passkey: abi.encode(15082030980405370419740072472485942557010062688916066938680834158844092834592, 7559725260680920969704920277862482544482567665366802956448675880212639145527); 14 | // user's safe: 0x2412292985aAc0F6696Bd473Fd845FC04e2C8DaD; 15 | // user's passkey: abi.encode(10747656110611756665682659753863643407259137457259800218403383070240064687361, 963539874084845106450594458182541423004553654316691203780793667511663756800); 16 | 17 | bytes32 public constant RECOVERY_METHOD = keccak256("recoverUserSafe"); 18 | 19 | IUserSafe userSafe = IUserSafe(0x2412292985aAc0F6696Bd473Fd845FC04e2C8DaD); 20 | bytes newOwnerBytes = abi.encode(10747656110611756665682659753863643407259137457259800218403383070240064687361, 963539874084845106450594458182541423004553654316691203780793667511663756800); 21 | 22 | function setUp() public override { 23 | super.setUp(); 24 | // address recoverySafe1 = 0xbfCe61CE31359267605F18dcE65Cb6c3cc9694A7; 25 | // address recoverySafe2 = 0xa265C271adbb0984EFd67310cfe85A77f449e291; 26 | 27 | // vm.startPrank(owner); 28 | // cashDataProvider.setEtherFiRecoverySigner(recoverySafe1); 29 | // cashDataProvider.setThirdPartyRecoverySigner(recoverySafe2); 30 | // vm.stopPrank(); 31 | } 32 | 33 | function test_BuildMessageHashAndDigest() external view { 34 | bytes32 messageHash = keccak256( 35 | abi.encode( 36 | RECOVERY_METHOD, 37 | block.chainid, 38 | address(userSafe), 39 | userSafe.nonce() + 1, 40 | newOwnerBytes 41 | ) 42 | ); 43 | 44 | bytes32 digest = messageHash.toEthSignedMessageHash(); 45 | 46 | console.log("digest: "); 47 | console.logBytes32(digest); 48 | console.log("messageHash: "); 49 | console.logBytes32(messageHash); 50 | } 51 | 52 | function test_RecoverWithSafeSignature() external { 53 | IUserSafe.Signature[2] memory signatures; 54 | 55 | signatures[0] = IUserSafe.Signature({ 56 | index: 1, // index 1 for etherfi signer safe 57 | signature: "" // no need to pass a sig since executed onchain 58 | }); 59 | 60 | signatures[1] = IUserSafe.Signature({ 61 | index: 2, // index 2 for third party safe 62 | signature: hex"72ec31877fb48e67e039bbb7ef83dc221a883559750ce4fd7a1b668a41cf8e361659b0159cc8fa4b405dd76bb405f66237ae57a5e231498fea8a2a8eae46673e1b" // no need to pass a sig since executed onchain 63 | }); 64 | 65 | userSafe.recoverUserSafe(newOwnerBytes, signatures); 66 | } 67 | } -------------------------------------------------------------------------------- /test/UserSafe/Swap.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, UserSafeEventEmitter, OwnerLib, UserSafeLib} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 6 | import {EIP1271SignatureUtils} from "../../src/libraries/EIP1271SignatureUtils.sol"; 7 | import {Setup, ERC20, IL2DebtManager} from "../Setup.t.sol"; 8 | import {OwnerLib} from "../../src/libraries/OwnerLib.sol"; 9 | 10 | contract UserSafeSwapTest is Setup { 11 | using MessageHashUtils for bytes32; 12 | using OwnerLib for address; 13 | 14 | function test_Swap() public { 15 | if (!isFork(chainId)) return; 16 | 17 | ERC20 weth = ERC20(chainConfig.weth); 18 | uint256 inputAmountToSwap = 1 ether; 19 | uint256 outputMinWethAmount = 0.9 ether; 20 | 21 | address[] memory assets = new address[](1); 22 | deal(address(weETH), address(aliceSafe), 1 ether); 23 | assets[0] = address(weETH); 24 | 25 | swapper.approveAssets(assets); 26 | 27 | bytes memory swapData = getQuoteOpenOcean( 28 | chainId, 29 | address(swapper), 30 | address(aliceSafe), 31 | assets[0], 32 | address(weth), 33 | inputAmountToSwap, 34 | ERC20(assets[0]).decimals() 35 | ); 36 | 37 | uint256 nonce = aliceSafe.nonce() + 1; 38 | bytes32 msgHash = keccak256( 39 | abi.encode( 40 | UserSafeLib.SWAP_METHOD, 41 | block.chainid, 42 | address(aliceSafe), 43 | nonce, 44 | address(assets[0]), 45 | address(weth), 46 | inputAmountToSwap, 47 | outputMinWethAmount, 48 | 0, 49 | swapData 50 | ) 51 | ); 52 | 53 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 54 | alicePk, 55 | msgHash.toEthSignedMessageHash() 56 | ); 57 | bytes memory signature = abi.encodePacked(r, s, v); 58 | 59 | uint256 aliceSafeWeETHBalBefore = ERC20(assets[0]).balanceOf(address(aliceSafe)); 60 | uint256 aliceSafeWethBalBefore = weth.balanceOf(address(aliceSafe)); 61 | assertEq(aliceSafeWeETHBalBefore, 1 ether); 62 | assertEq(aliceSafeWethBalBefore, 0); 63 | 64 | aliceSafe.swap( 65 | address(assets[0]), 66 | address(weth), 67 | inputAmountToSwap, 68 | outputMinWethAmount, 69 | 0, 70 | swapData, 71 | signature 72 | ); 73 | 74 | uint256 aliceSafeWeETHBalAfter = ERC20(assets[0]).balanceOf(address(aliceSafe)); 75 | uint256 aliceSafeWethBalAfter = weth.balanceOf(address(aliceSafe)); 76 | 77 | assertEq(aliceSafeWeETHBalAfter, 0); 78 | assertGt(aliceSafeWethBalAfter, outputMinWethAmount); 79 | } 80 | 81 | function test_CannotSwapIfBorrowPositionBecomesUnhealthy() external { 82 | if (!isFork(chainId)) return; 83 | uint256 collateralBal = 1 ether; 84 | deal(address(weETH), address(aliceSafe), collateralBal); 85 | deal(address(usdc), address(debtManager), 1000e6); 86 | 87 | _setMode(IUserSafe.Mode.Credit); 88 | vm.warp(aliceSafe.incomingCreditModeStartTime() + 1); 89 | 90 | vm.prank(etherFiWallet); 91 | aliceSafe.spend(txId, address(usdc), 100e6); 92 | 93 | ERC20 weth = ERC20(chainConfig.weth); 94 | uint256 inputAmountToSwap = collateralBal; 95 | uint256 outputMinWethAmount = 0.9 ether; 96 | 97 | address[] memory assets = new address[](1); 98 | assets[0] = address(weETH); 99 | 100 | swapper.approveAssets(assets); 101 | 102 | bytes memory swapData = getQuoteOpenOcean( 103 | chainId, 104 | address(swapper), 105 | address(aliceSafe), 106 | assets[0], 107 | address(weth), 108 | inputAmountToSwap, 109 | ERC20(assets[0]).decimals() 110 | ); 111 | 112 | uint256 nonce = aliceSafe.nonce() + 1; 113 | bytes32 msgHash = keccak256( 114 | abi.encode( 115 | UserSafeLib.SWAP_METHOD, 116 | block.chainid, 117 | address(aliceSafe), 118 | nonce, 119 | address(assets[0]), 120 | address(weth), 121 | inputAmountToSwap, 122 | outputMinWethAmount, 123 | 0, 124 | swapData 125 | ) 126 | ); 127 | 128 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 129 | alicePk, 130 | msgHash.toEthSignedMessageHash() 131 | ); 132 | bytes memory signature = abi.encodePacked(r, s, v); 133 | 134 | vm.expectRevert(IL2DebtManager.AccountUnhealthy.selector); 135 | aliceSafe.swap( 136 | address(assets[0]), 137 | address(weth), 138 | inputAmountToSwap, 139 | outputMinWethAmount, 140 | 0, 141 | swapData, 142 | signature 143 | ); 144 | } 145 | 146 | function _setMode(IUserSafe.Mode mode) internal { 147 | uint256 nonce = aliceSafe.nonce() + 1; 148 | bytes32 msgHash = keccak256( 149 | abi.encode( 150 | UserSafeLib.SET_MODE_METHOD, 151 | block.chainid, 152 | address(aliceSafe), 153 | nonce, 154 | mode 155 | ) 156 | ); 157 | 158 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 159 | alicePk, 160 | msgHash.toEthSignedMessageHash() 161 | ); 162 | 163 | bytes memory signature = abi.encodePacked(r, s, v); 164 | aliceSafe.setMode(mode, signature); 165 | } 166 | } -------------------------------------------------------------------------------- /test/UserSafe/UserSafeFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test, stdError} from "forge-std/Test.sol"; 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import {UserSafeFactory} from "../../src/user-safe/UserSafeFactory.sol"; 7 | import {UserSafeV2Mock} from "../../src/mocks/UserSafeV2Mock.sol"; 8 | import {Swapper1InchV6} from "../../src/utils/Swapper1InchV6.sol"; 9 | import {PriceProvider} from "../../src/oracle/PriceProvider.sol"; 10 | import {CashDataProvider} from "../../src/utils/CashDataProvider.sol"; 11 | import {Setup, IUserSafe, UserSafeCore} from "../Setup.t.sol"; 12 | import {OwnerLib} from "../../src/libraries/OwnerLib.sol"; 13 | 14 | error OwnableUnauthorizedAccount(address account); 15 | 16 | contract UserSafeFactoryV2 is UserSafeFactory { 17 | function version() external pure returns (uint256) { 18 | return 2; 19 | } 20 | } 21 | 22 | contract UserSafeFactoryTest is Setup { 23 | using OwnerLib for address; 24 | 25 | UserSafeV2Mock implV2; 26 | 27 | address bob = makeAddr("bob"); 28 | bytes bobBytes = abi.encode(bob); 29 | 30 | IUserSafe bobSafe; 31 | bytes saltData = bytes("bobSafe"); 32 | 33 | 34 | function setUp() public override { 35 | super.setUp(); 36 | 37 | implV2 = new UserSafeV2Mock(address(cashDataProvider)); 38 | 39 | vm.prank(owner); 40 | bobSafe = IUserSafe( 41 | factory.createUserSafe( 42 | saltData, 43 | abi.encodeWithSelector( 44 | UserSafeCore.initialize.selector, 45 | bobBytes, 46 | defaultDailySpendingLimit, 47 | defaultMonthlySpendingLimit, 48 | timezoneOffset 49 | ) 50 | ) 51 | ); 52 | 53 | vm.stopPrank(); 54 | } 55 | 56 | function test_Deploy() public view { 57 | address deterministicAddress = factory.getUserSafeAddress( 58 | saltData, 59 | abi.encodeWithSelector( 60 | UserSafeCore.initialize.selector, 61 | bobBytes, 62 | defaultDailySpendingLimit, 63 | defaultMonthlySpendingLimit, 64 | timezoneOffset 65 | )); 66 | 67 | assertEq(deterministicAddress, address(bobSafe)); 68 | assertEq(aliceSafe.owner().ethAddr, alice); 69 | assertEq(bobSafe.owner().ethAddr, bob); 70 | } 71 | 72 | function test_UpgradeUserSafeImpl() public { 73 | vm.prank(owner); 74 | factory.upgradeUserSafeCoreImpl(address(implV2)); 75 | 76 | UserSafeV2Mock aliceSafeV2 = UserSafeV2Mock(address(aliceSafe)); 77 | UserSafeV2Mock bobSafeV2 = UserSafeV2Mock(address(bobSafe)); 78 | 79 | assertEq(aliceSafeV2.version(), 2); 80 | assertEq(bobSafeV2.version(), 2); 81 | } 82 | 83 | function test_UpgradeUserSafeFactory() public { 84 | UserSafeFactoryV2 factoryV2 = new UserSafeFactoryV2(); 85 | 86 | vm.prank(owner); 87 | factory.upgradeToAndCall(address(factoryV2), ""); 88 | 89 | assertEq(UserSafeFactoryV2(address(factory)).version(), 2); 90 | } 91 | 92 | function test_OnlyOwnerCanUpgradeUserSafeImpl() public { 93 | vm.prank(notOwner); 94 | vm.expectRevert(buildAccessControlRevertData(notOwner, DEFAULT_ADMIN_ROLE)); 95 | factory.upgradeUserSafeCoreImpl(address(implV2)); 96 | } 97 | 98 | function test_OnlyOwnerCanUpgradeFactoryImpl() public { 99 | vm.prank(notOwner); 100 | vm.expectRevert(buildAccessControlRevertData(notOwner, DEFAULT_ADMIN_ROLE)); 101 | factory.upgradeToAndCall(address(1), ""); 102 | } 103 | 104 | function test_OnlyAdminCanCreateUserSafe() public { 105 | vm.prank(notOwner); 106 | vm.expectRevert(buildAccessControlRevertData(notOwner, ADMIN_ROLE)); 107 | factory.createUserSafe( 108 | saltData, 109 | abi.encodeWithSelector( 110 | UserSafeCore.initialize.selector, 111 | hex"112345", 112 | defaultDailySpendingLimit, 113 | defaultMonthlySpendingLimit, 114 | timezoneOffset 115 | ) 116 | ); 117 | } 118 | 119 | function test_OnlyAdminCanSetBeacon() public { 120 | vm.prank(notOwner); 121 | vm.expectRevert(buildAccessControlRevertData(notOwner, DEFAULT_ADMIN_ROLE)); 122 | factory.setBeacon(address(1)); 123 | } 124 | 125 | function test_SetBeacon() public { 126 | address newBeacon = address(1); 127 | 128 | vm.startPrank(owner); 129 | vm.expectEmit(true, true, true, true); 130 | emit UserSafeFactory.BeaconSet(factory.beacon(), newBeacon); 131 | factory.setBeacon(newBeacon); 132 | assertEq(factory.beacon(), newBeacon); 133 | vm.stopPrank(); 134 | } 135 | 136 | function test_CannotSetBeaconToAddressZero() public { 137 | vm.prank(owner); 138 | vm.expectRevert(UserSafeFactory.InvalidValue.selector); 139 | factory.setBeacon(address(0)); 140 | } 141 | 142 | function test_OnlyAdminCanSetCashDataProvider() public { 143 | vm.prank(notOwner); 144 | vm.expectRevert(buildAccessControlRevertData(notOwner, DEFAULT_ADMIN_ROLE)); 145 | factory.setCashDataProvider(address(1)); 146 | } 147 | 148 | function test_SetCashDataProvider() public { 149 | address newCashDataProvider = address(1); 150 | vm.startPrank(owner); 151 | vm.expectEmit(true, true, true, true); 152 | emit UserSafeFactory.CashDataProviderSet(factory.cashDataProvider(), newCashDataProvider); 153 | factory.setCashDataProvider(newCashDataProvider); 154 | assertEq(factory.cashDataProvider(), newCashDataProvider); 155 | vm.stopPrank(); 156 | } 157 | 158 | function test_CannotSetCashDataProviderToAddressZero() public { 159 | vm.prank(owner); 160 | vm.expectRevert(UserSafeFactory.InvalidValue.selector); 161 | factory.setCashDataProvider(address(0)); 162 | } 163 | } -------------------------------------------------------------------------------- /test/UserSafe/WebAuthn.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {IUserSafe, OwnerLib, UserSafeLib, UserSafeCore} from "../../src/user-safe/UserSafeCore.sol"; 5 | import {WebAuthn} from "../../src/libraries/WebAuthn.sol"; 6 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 7 | import {Setup} from "../Setup.t.sol"; 8 | import {WebAuthnInfo, WebAuthnUtils} from "../WebAuthnUtils.sol"; 9 | 10 | contract UserSafeWebAuthnSignatureTest is Setup { 11 | uint256 passkeyPrivateKey = 12 | uint256( 13 | 0x03d99692017473e2d631945a812607b23269d85721e0f370b8d3e7d29a874fd2 14 | ); 15 | bytes passkeyOwner = 16 | hex"1c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d"; 17 | 18 | IUserSafe passkeyOwnerSafe; 19 | 20 | function setUp() public override { 21 | super.setUp(); 22 | 23 | bytes memory saltData = bytes("passkeySafe"); 24 | 25 | vm.prank(owner); 26 | passkeyOwnerSafe = IUserSafe( 27 | factory.createUserSafe( 28 | saltData, 29 | abi.encodeWithSelector( 30 | UserSafeCore.initialize.selector, 31 | passkeyOwner, 32 | defaultDailySpendingLimit, 33 | defaultMonthlySpendingLimit, 34 | timezoneOffset 35 | ) 36 | ) 37 | ); 38 | } 39 | 40 | function test_Deploy() public view { 41 | assertEq( 42 | abi.encode(passkeyOwnerSafe.owner().x, passkeyOwnerSafe.owner().y), 43 | passkeyOwner 44 | ); 45 | 46 | assertEq( 47 | abi.encode( 48 | passkeyOwnerSafe.recoverySigners()[0].x, 49 | passkeyOwnerSafe.recoverySigners()[0].y 50 | ), 51 | abi.encode(0, 0) 52 | ); 53 | assertEq(passkeyOwnerSafe.recoverySigners()[0].ethAddr, address(0)); 54 | assertEq( 55 | passkeyOwnerSafe.recoverySigners()[1].ethAddr, 56 | etherFiRecoverySigner 57 | ); 58 | assertEq( 59 | passkeyOwnerSafe.recoverySigners()[2].ethAddr, 60 | thirdPartyRecoverySigner 61 | ); 62 | } 63 | 64 | function test_CanSetOwnerWithWebAuthn() public { 65 | address newOwner = makeAddr("owner"); 66 | uint256 nonce = passkeyOwnerSafe.nonce() + 1; 67 | bytes memory newOwnerBytes = abi.encode(newOwner); 68 | 69 | bytes32 msgHash = keccak256( 70 | abi.encode( 71 | UserSafeLib.SET_OWNER_METHOD, 72 | block.chainid, 73 | address(passkeyOwnerSafe), 74 | nonce, 75 | newOwnerBytes 76 | ) 77 | ); 78 | 79 | WebAuthnInfo memory webAuthn = WebAuthnUtils.getWebAuthnStruct(msgHash); 80 | 81 | // a user -> change my spending limit 82 | // challenge, clientjson 83 | // take a signature using passkey, UI gives authenticator data -> user biometrics 84 | (bytes32 r, bytes32 s) = vm.signP256( 85 | passkeyPrivateKey, 86 | webAuthn.messageHash 87 | ); 88 | s = bytes32(WebAuthnUtils.normalizeS(uint256(s))); 89 | 90 | bytes memory signature = abi.encode( 91 | WebAuthn.WebAuthnAuth({ 92 | authenticatorData: webAuthn.authenticatorData, 93 | clientDataJSON: webAuthn.clientDataJSON, 94 | typeIndex: 1, 95 | challengeIndex: 23, 96 | r: uint256(r), 97 | s: uint256(s) 98 | }) 99 | ); 100 | 101 | passkeyOwnerSafe.setOwner(newOwnerBytes, signature); 102 | 103 | assertEq(passkeyOwnerSafe.owner().ethAddr, newOwner); 104 | } 105 | } -------------------------------------------------------------------------------- /test/WebAuthnUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {n} from "SmoothCryptoLib/lib/libSCL_RIP7212.sol"; 5 | import {Base64Url} from "../src/libraries/Base64Url.sol"; 6 | 7 | struct WebAuthnInfo { 8 | bytes authenticatorData; 9 | string clientDataJSON; 10 | bytes32 messageHash; 11 | } 12 | 13 | library WebAuthnUtils { 14 | uint256 constant P256_N_DIV_2 = n / 2; 15 | 16 | function getWebAuthnStruct( 17 | bytes32 challenge 18 | ) public pure returns (WebAuthnInfo memory) { 19 | string memory challengeb64url = Base64Url.encode(abi.encode(challenge)); 20 | string memory clientDataJSON = string( 21 | abi.encodePacked( 22 | '{"type":"webauthn.get","challenge":"', 23 | challengeb64url, 24 | '","origin":"https://cash.ether.fi","crossOrigin":false}' 25 | ) 26 | ); 27 | 28 | // Authenticator data for Chrome Profile touchID signature 29 | bytes 30 | memory authenticatorData = hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000"; 31 | 32 | bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON)); 33 | bytes32 messageHash = sha256( 34 | abi.encodePacked(authenticatorData, clientDataJSONHash) 35 | ); 36 | 37 | return WebAuthnInfo(authenticatorData, clientDataJSON, messageHash); 38 | } 39 | 40 | /// @dev normalizes the s value from a p256r1 signature so that 41 | /// it will pass malleability checks. 42 | function normalizeS(uint256 s) public pure returns (uint256) { 43 | if (s > P256_N_DIV_2) { 44 | return n - s; 45 | } 46 | 47 | return s; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/getQuoteOpenOcean.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { ethers } from "ethers"; 3 | import dotenv from "dotenv"; 4 | dotenv.config(); 5 | 6 | function chainIdToChainName(chainId:string) : string { 7 | if (chainId === "534352") return "scroll"; 8 | else if (chainId === "42161") return "arbitrum"; 9 | else throw new Error("Chain ID unidentified"); 10 | } 11 | 12 | const OPEN_OCEAN_API_ENDPOINT = 13 | `https://open-api.openocean.finance/v3`; 14 | const OPEN_OCEAN_ROUTER = "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64"; 15 | const SELECTOR = "0x90411a32" 16 | 17 | const ABI = [ 18 | { 19 | "inputs": [ 20 | { 21 | "internalType": "contract IOpenOceanCaller", 22 | "name": "caller", 23 | "type": "address" 24 | }, 25 | { 26 | "components": [ 27 | { 28 | "internalType": "contract IERC20", 29 | "name": "srcToken", 30 | "type": "address" 31 | }, 32 | { 33 | "internalType": "contract IERC20", 34 | "name": "dstToken", 35 | "type": "address" 36 | }, 37 | { 38 | "internalType": "address", 39 | "name": "srcReceiver", 40 | "type": "address" 41 | }, 42 | { 43 | "internalType": "address", 44 | "name": "dstReceiver", 45 | "type": "address" 46 | }, 47 | { "internalType": "uint256", "name": "amount", "type": "uint256" }, 48 | { 49 | "internalType": "uint256", 50 | "name": "minReturnAmount", 51 | "type": "uint256" 52 | }, 53 | { 54 | "internalType": "uint256", 55 | "name": "guaranteedAmount", 56 | "type": "uint256" 57 | }, 58 | { "internalType": "uint256", "name": "flags", "type": "uint256" }, 59 | { "internalType": "address", "name": "referrer", "type": "address" }, 60 | { "internalType": "bytes", "name": "permit", "type": "bytes" } 61 | ], 62 | "internalType": "struct OpenOceanExchange.SwapDescription", 63 | "name": "desc", 64 | "type": "tuple" 65 | }, 66 | { 67 | "components": [ 68 | { "internalType": "uint256", "name": "target", "type": "uint256" }, 69 | { "internalType": "uint256", "name": "gasLimit", "type": "uint256" }, 70 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 71 | { "internalType": "bytes", "name": "data", "type": "bytes" } 72 | ], 73 | "internalType": "struct IOpenOceanCaller.CallDescription[]", 74 | "name": "calls", 75 | "type": "tuple[]" 76 | } 77 | ], 78 | "name": "swap", 79 | "outputs": [ 80 | { "internalType": "uint256", "name": "returnAmount", "type": "uint256" } 81 | ], 82 | "stateMutability": "payable", 83 | "type": "function" 84 | } 85 | ]; 86 | 87 | export const getData = async () => { 88 | const args = process.argv; 89 | const chainId = args[2]; 90 | const fromAddress = args[3]; 91 | const toAddress = args[4]; 92 | const fromAsset = args[5]; 93 | const toAsset = args[6]; 94 | const fromAmount = args[7]; 95 | const fromAssetDecimals = args[8]; 96 | 97 | const data = await getOpenOceanSwapData({ 98 | chainId, 99 | fromAddress, 100 | toAddress, 101 | fromAsset, 102 | toAsset, 103 | fromAmount, 104 | fromAssetDecimals 105 | }); 106 | 107 | console.log(recodeSwapData(data)); 108 | }; 109 | 110 | const recodeSwapData = (apiEncodedData: string): string => { 111 | try { 112 | const cOpenOceanRouter = new ethers.Contract( 113 | OPEN_OCEAN_ROUTER, 114 | new ethers.utils.Interface(ABI) 115 | ); 116 | 117 | // decode the 1Inch tx.data that is RLP encoded 118 | const swapTx = cOpenOceanRouter.interface.parseTransaction({ 119 | data: apiEncodedData, 120 | }); 121 | 122 | const encodedData = ethers.utils.defaultAbiCoder.encode( 123 | ["bytes4","address","tuple(uint256,uint256,uint256,bytes)[]"], 124 | [SELECTOR, swapTx.args[0], swapTx.args[2]] 125 | ); 126 | 127 | return encodedData; 128 | } catch (err: any) { 129 | throw Error(`Failed to recode OpenOcean swap data: ${err.message}`); 130 | } 131 | } 132 | 133 | const getOpenOceanSwapData = async ({ 134 | chainId, 135 | fromAddress, 136 | toAddress, 137 | fromAsset, 138 | toAsset, 139 | fromAmount, 140 | fromAssetDecimals 141 | }: { 142 | chainId: string; 143 | fromAddress: string; 144 | toAddress: string; 145 | fromAsset: string; 146 | toAsset: string; 147 | fromAmount: string; 148 | fromAssetDecimals: string; 149 | }) => { 150 | const params = { 151 | inTokenAddress: fromAsset, 152 | outTokenAddress: toAsset, 153 | amount: ethers.utils.formatUnits(fromAmount.toString(), fromAssetDecimals.toString()).toString(), 154 | sender: fromAddress, 155 | account: toAddress, 156 | slippage: 1, 157 | gasPrice: 0.05, 158 | }; 159 | 160 | let retries = 5; 161 | 162 | const API_ENDPOINT = `${OPEN_OCEAN_API_ENDPOINT}/${chainIdToChainName(chainId)}/swap_quote`; 163 | 164 | while (retries > 0) { 165 | try { 166 | const response = await axios.get(API_ENDPOINT, { 167 | params, 168 | }); 169 | 170 | if (!response.data.data || !response.data.data.data) { 171 | console.error(response.data); 172 | throw Error("response is missing data.data"); 173 | } 174 | 175 | return response.data.data.data; 176 | } catch (err: any) { 177 | if (err.response) { 178 | console.error("Response data : ", err.response.data); 179 | console.error("Response status: ", err.response.status); 180 | } 181 | if (err.response?.status == 429) { 182 | retries = retries - 1; 183 | // Wait for 2s before next try 184 | await new Promise((r) => setTimeout(r, 2000)); 185 | continue; 186 | } 187 | throw Error(`Call to OpenOcean swap API failed: ${err.message}`); 188 | } 189 | } 190 | 191 | throw Error(`Call to OpenOcean swap API failed: Rate-limited`); 192 | }; 193 | 194 | getData(); -------------------------------------------------------------------------------- /test/proposeRecoverySignature.ts: -------------------------------------------------------------------------------- 1 | // proposeRecoverySignature.ts 2 | 3 | import SafeApiKit from "@safe-global/api-kit"; 4 | import Safe from "@safe-global/protocol-kit"; 5 | import {MetaTransactionData, OperationType} from "@safe-global/types-kit"; 6 | import {config} from "dotenv"; 7 | 8 | config(); 9 | 10 | async function propose() { 11 | const args = process.argv; 12 | const digest = args[2]; 13 | 14 | const scrollRpc = process.env.SCROLL_RPC; 15 | const proposerPrivateKey = process.env.PROPOSER_PRIVATE_KEY; 16 | const proposerAddress = process.env.PROPOSER_ADDRESS; 17 | const recoverySafeAddress = "0xbED1b10aF02D48DA7dA0Fff26d16E0873AF46706"; 18 | const safeSignMessageLib = "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9"; 19 | 20 | if (scrollRpc === undefined) throw new Error("SCROLL_RPC not found in .env"); 21 | if (proposerPrivateKey === undefined) throw new Error("PROPOSER_PRIVATE_KEY not found in .env"); 22 | if (proposerAddress === undefined) throw new Error("PROPOSER_ADDRESS not found in .env"); 23 | 24 | const apiKit = new SafeApiKit({ 25 | chainId: 534352n 26 | }); 27 | 28 | const protocolKitOwner1 = await Safe.init({ 29 | provider: scrollRpc, 30 | signer: proposerPrivateKey, 31 | safeAddress: recoverySafeAddress 32 | }); 33 | 34 | const safeTransactionData: MetaTransactionData = { 35 | to: safeSignMessageLib, 36 | value: "0", 37 | data: "0x85a5affe00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020" + digest.slice(2), 38 | operation: OperationType.DelegateCall 39 | }; 40 | 41 | const safeTransaction = await protocolKitOwner1.createTransaction({ 42 | transactions: [safeTransactionData] 43 | }); 44 | 45 | const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction); 46 | const signature = await protocolKitOwner1.signHash(safeTxHash); 47 | 48 | await apiKit.proposeTransaction({ 49 | safeAddress: recoverySafeAddress, 50 | safeTransactionData: safeTransaction.data, 51 | safeTxHash, 52 | senderAddress: proposerAddress, 53 | senderSignature: signature.data 54 | }); 55 | } 56 | 57 | propose(); -------------------------------------------------------------------------------- /utils/GnosisHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 6 | 7 | 8 | contract GnosisHelpers is Script { 9 | 10 | /** 11 | * @dev Simulations the execution of a gnosis transaction bundle on the current fork 12 | * @param transactionPath The path to the transaction bundle json file 13 | * @param sender The address of the gnosis safe that will execute the transaction 14 | */ 15 | function executeGnosisTransactionBundle(string memory transactionPath, address sender) public { 16 | string memory json = vm.readFile(transactionPath); 17 | for (uint256 i = 0; vm.keyExistsJson(json, string.concat(".transactions[", Strings.toString(i), "]")); i++) { 18 | address to = vm.parseJsonAddress(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].to")); 19 | uint256 value = vm.parseJsonUint(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].value")); 20 | bytes memory data = vm.parseJsonBytes(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].data")); 21 | 22 | vm.prank(sender); 23 | (bool success,) = address(to).call{value: value}(data); 24 | require(success, "Transaction failed"); 25 | } 26 | } 27 | 28 | 29 | // Get the gnosis transaction header 30 | function _getGnosisHeader(string memory chainId) internal pure returns (string memory) { 31 | return string.concat('{"chainId":"', chainId, '","meta": { "txBuilderVersion": "1.16.5" }, "transactions": ['); 32 | } 33 | 34 | // Create a gnosis transaction 35 | // ether sent value is always 0 for our usecase 36 | function _getGnosisTransaction(string memory to, string memory data, bool isLast) internal pure returns (string memory) { 37 | string memory suffix = isLast ? ']}' : ','; 38 | return string.concat('{"to":"', to, '","value":"0","data":"', data, '"}', suffix); 39 | } 40 | 41 | // Helper function to convert bytes to hex strings 42 | // soldity encodes returns a bytes object and this must be converted to a hex string to be used in gnosis transactions 43 | function iToHex(bytes memory buffer) public pure returns (string memory) { 44 | // Fixed buffer size for hexadecimal convertion 45 | bytes memory converted = new bytes(buffer.length * 2); 46 | 47 | bytes memory _base = "0123456789abcdef"; 48 | 49 | for (uint256 i = 0; i < buffer.length; i++) { 50 | converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; 51 | converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; 52 | } 53 | 54 | return string(abi.encodePacked("0x", converted)); 55 | } 56 | 57 | // Helper function to convert an address to a hex string of the bytes 58 | function addressToHex(address addr) public pure returns (string memory) { 59 | return iToHex(abi.encodePacked(addr)); 60 | } 61 | 62 | address constant timelock = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; 63 | bytes32 constant predecessor = 0x0000000000000000000000000000000000000000000000000000000000000000; 64 | bytes32 constant salt = 0x0000000000000000000000000000000000000000000000000000000000000000; 65 | uint256 constant delay = 259200; 66 | 67 | // Generates the schedule transaction for a Timelock 68 | function _getTimelockScheduleTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { 69 | 70 | string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); 71 | string memory scheduleTransactionData = iToHex(abi.encodeWithSignature("schedule(address,uint256,bytes,bytes32,bytes32,uint256)", to, 0, data, predecessor, salt, delay)); 72 | 73 | return _getGnosisTransaction(timelockAddressHex, scheduleTransactionData, isLasts); 74 | } 75 | 76 | function _getTimelockExecuteTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { 77 | 78 | string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); 79 | string memory executeTransactionData = iToHex(abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", to, 0, data, predecessor, salt)); 80 | 81 | return _getGnosisTransaction(timelockAddressHex, executeTransactionData, isLasts); 82 | } 83 | 84 | } 85 | --------------------------------------------------------------------------------