diff --git a/.gitignore b/.gitignore index 847bb491de..3315b137ca 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,10 @@ contracts/.localKeyValueStorage contracts/.localKeyValueStorage.mainnet contracts/.localKeyValueStorage.holesky contracts/scripts/defender-actions/dist/ +contracts/broadcast/ +contracts/out/ +cache/ +dependencies/ contracts/lib/defender-actions/dist/ @@ -89,3 +93,8 @@ unit-coverage # Certora # .certora_internal + + +# Soldeer +foundry/dependencies +soldeer.lock diff --git a/contracts/Makefile b/contracts/Makefile new file mode 100644 index 0000000000..598629fcb1 --- /dev/null +++ b/contracts/Makefile @@ -0,0 +1,57 @@ +-include .env + +.EXPORT_ALL_VARIABLES: +MAKEFLAGS += --no-print-directory + +# ANSI Colors +RESET=\033[0m + +BLACK=\033[0;30m +RED=\033[0;31m +GREEN=\033[0;32m +YELLOW=\033[0;33m +BLUE=\033[0;34m +MAGENTA=\033[0;35m +CYAN=\033[0;36m +WHITE=\033[0;37m + +# Bold colors +BOLD_BLACK=\033[1;30m +BOLD_RED=\033[1;31m +BOLD_GREEN=\033[1;32m +BOLD_YELLOW=\033[1;33m +BOLD_BLUE=\033[1;34m +BOLD_MAGENTA=\033[1;35m +BOLD_CYAN=\033[1;36m +BOLD_WHITE=\033[1;37m + +DATE ?= $(shell date +%Y_%m) +CHAIN ?= Mainnet + +VALID_CHAINS = Mainnet Base Sonic Plume Arbitrum Holesky +MAINNET_ID = 1 +BASE_ID = 8453 +SONIC_ID = 146 +PLUME_ID = 98865 +ARBITRUM_ID = 42161 +HOLESKY_ID = 17000 + +define get_chain_id +$(if $(filter $(CHAIN),$(VALID_CHAINS)),\ +$(if $(filter Mainnet,$(CHAIN)),$(MAINNET_ID),\ +$(if $(filter Base,$(CHAIN)),$(BASE_ID),\ +$(if $(filter Sonic,$(CHAIN)),$(SONIC_ID),\ +$(if $(filter Plume,$(CHAIN)),$(PLUME_ID),\ +$(if $(filter Arbitrum,$(CHAIN)),$(ARBITRUM_ID),\ +$(HOLESKY_ID)))))),\ +$(error CHAIN "$(CHAIN)" is not valid. Valid chains are: $(VALID_CHAINS))) +endef + +script: + $(eval CHAIN_ID := $(call get_chain_id)) + @echo "$(BOLD_BLUE)Running script RunlogsDATE=$(DATE)_CHAIN=$(CHAIN) $(RESET) \n" + forge script Runlogs_$(DATE)_$(CHAIN) -vvvvv + @echo "$(BOLD_BLUE)\nConverting foundry broadcast messages to Safe format... $(RESET) \n" + forge script BroadcastConvertor contracts/broadcast/$(DATE).s.sol/$(CHAIN_ID)/dry-run/ + +.PHONY: script \ No newline at end of file diff --git a/contracts/contracts/interfaces/arm/ISonicARM.sol b/contracts/contracts/interfaces/arm/ISonicARM.sol new file mode 100644 index 0000000000..5f166cc779 --- /dev/null +++ b/contracts/contracts/interfaces/arm/ISonicARM.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ISonicARM { + error ERC20InsufficientAllowance( + address spender, + uint256 allowance, + uint256 needed + ); + error ERC20InsufficientBalance( + address sender, + uint256 balance, + uint256 needed + ); + error ERC20InvalidApprover(address approver); + error ERC20InvalidReceiver(address receiver); + error ERC20InvalidSender(address sender); + error ERC20InvalidSpender(address spender); + error InvalidInitialization(); + error NotInitializing(); + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + error SafeCastOverflowedIntToUint(int256 value); + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + error SafeCastOverflowedUintToInt(uint256 value); + + event ARMBufferUpdated(uint256 armBuffer); + event ActiveMarketUpdated(address indexed market); + event AdminChanged(address previousAdmin, address newAdmin); + event Allocated(address indexed market, int256 assets); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + event CapManagerUpdated(address indexed capManager); + event ClaimOriginWithdrawals(uint256[] requestIds, uint256 amountClaimed); + event CrossPriceUpdated(uint256 crossPrice); + event Deposit(address indexed owner, uint256 assets, uint256 shares); + event FeeCollected(address indexed feeCollector, uint256 fee); + event FeeCollectorUpdated(address indexed newFeeCollector); + event FeeUpdated(uint256 fee); + event Initialized(uint64 version); + event MarketAdded(address indexed market); + event MarketRemoved(address indexed market); + event OperatorChanged(address newAdmin); + event RedeemClaimed( + address indexed withdrawer, + uint256 indexed requestId, + uint256 assets + ); + event RedeemRequested( + address indexed withdrawer, + uint256 indexed requestId, + uint256 assets, + uint256 queued, + uint256 claimTimestamp + ); + event RequestOriginWithdrawal(uint256 amount, uint256 requestId); + event TraderateChanged(uint256 traderate0, uint256 traderate1); + event Transfer(address indexed from, address indexed to, uint256 value); + + function FEE_SCALE() external view returns (uint256); + + function MAX_CROSS_PRICE_DEVIATION() external view returns (uint256); + + function PRICE_SCALE() external view returns (uint256); + + function activeMarket() external view returns (address); + + function addMarkets(address[] memory _markets) external; + + function allocate() external returns (int256 liquidityDelta); + + function allocateThreshold() external view returns (int256); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function armBuffer() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function baseAsset() external view returns (address); + + function capManager() external view returns (address); + + function claimDelay() external view returns (uint256); + + function claimOriginWithdrawals(uint256[] memory requestIds) + external + returns (uint256 amountClaimed); + + function claimRedeem(uint256 requestId) external returns (uint256 assets); + + function claimable() external view returns (uint256 claimableAmount); + + function collectFees() external returns (uint256 fees); + + function convertToAssets(uint256 shares) + external + view + returns (uint256 assets); + + function convertToShares(uint256 assets) + external + view + returns (uint256 shares); + + function crossPrice() external view returns (uint256); + + function decimals() external view returns (uint8); + + function deposit(uint256 assets, address receiver) + external + returns (uint256 shares); + + function deposit(uint256 assets) external returns (uint256 shares); + + function fee() external view returns (uint16); + + function feeCollector() external view returns (address); + + function feesAccrued() external view returns (uint256 fees); + + function initialize( + string memory _name, + string memory _symbol, + address _operator, + uint256 _fee, + address _feeCollector, + address _capManager + ) external; + + function lastAvailableAssets() external view returns (int128); + + function liquidityAsset() external view returns (address); + + function minSharesToRedeem() external view returns (uint256); + + function name() external view returns (string memory); + + function nextWithdrawalIndex() external view returns (uint256); + + function operator() external view returns (address); + + function owner() external view returns (address); + + function previewDeposit(uint256 assets) + external + view + returns (uint256 shares); + + function previewRedeem(uint256 shares) + external + view + returns (uint256 assets); + + function removeMarket(address _market) external; + + function requestOriginWithdrawal(uint256 amount) + external + returns (uint256 requestId); + + function requestRedeem(uint256 shares) + external + returns (uint256 requestId, uint256 assets); + + function setARMBuffer(uint256 _armBuffer) external; + + function setActiveMarket(address _market) external; + + function setCapManager(address _capManager) external; + + function setCrossPrice(uint256 newCrossPrice) external; + + function setFee(uint256 _fee) external; + + function setFeeCollector(address _feeCollector) external; + + function setOperator(address newOperator) external; + + function setOwner(address newOwner) external; + + function setPrices(uint256 buyT1, uint256 sellT1) external; + + function supportedMarkets(address market) + external + view + returns (bool supported); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForTokens( + address inToken, + address outToken, + uint256 amountIn, + uint256 amountOutMin, + address to + ) external; + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + address inToken, + address outToken, + uint256 amountOut, + uint256 amountInMax, + address to + ) external; + + function symbol() external view returns (string memory); + + function token0() external view returns (address); + + function token1() external view returns (address); + + function totalAssets() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function traderate0() external view returns (uint256); + + function traderate1() external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + function vault() external view returns (address); + + function vaultWithdrawalAmount() external view returns (uint256); + + function withdrawalRequests(uint256 requestId) + external + view + returns ( + address withdrawer, + bool claimed, + uint40 claimTimestamp, + uint128 assets, + uint128 queued + ); + + function withdrawsClaimed() external view returns (uint128); + + function withdrawsQueued() external view returns (uint128); +} diff --git a/contracts/scripts/runlogs/2025_08.s.sol b/contracts/scripts/runlogs/2025_08.s.sol new file mode 100644 index 0000000000..708ea97611 --- /dev/null +++ b/contracts/scripts/runlogs/2025_08.s.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Setup +import { SetupBase } from "./utils/Setup.sol"; +import { SetupSonic } from "./utils/Setup.sol"; +import { SetupMainnet } from "./utils/Setup.sol"; + +import { Sonic } from "./utils/Addresses.sol"; + +// Foundry +import { console } from "forge-std/console.sol"; + +contract Runlogs_2025_08_Mainnet is SetupMainnet { + function run() public { + _2025_08_04(); + } + + // ------------------------------------------------------------------ + // July 28, 2025 - Withdraw 2335 WETH from new Curve AMO + // ------------------------------------------------------------------ + function _2025_08_04() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToWithdraw = 1_000 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToWithdraw; + oethVaultAdmin.withdrawFromStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e", profit); + console.log("OETH Supply change: %18e", supplyChange); + console.log("Vault value change: %18e", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } +} + +contract Runlogs_2025_08_Base is SetupBase { + function run() public { + _2025_08_18(); + _2025_08_21(); + _2025_08_28(); + } + + // ------------------------------------------------------------------ + // August 18, 2025 - Deposit 90 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_08_18() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 90 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // August 21, 2025 - Deposit 125 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_08_21() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 125 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // August 28, 2025 - Deposit 85 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_08_28() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 85 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 1 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(0, 1, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } +} + +contract Runlogs_2025_08_Sonic is SetupSonic { + function run() public { + _2025_08_29(); + } + + function _2025_08_29() internal { + vm.startBroadcast(timelock); + // Before + uint256 balance = os.balanceOf(address(Sonic.ARM)); + + // Main action + arm.requestOriginWithdrawal(balance); + arm.setCrossPrice(0.9999e36); + arm.setFeeCollector(0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c); + + // After + vm.stopBroadcast(); + } +} diff --git a/contracts/scripts/runlogs/2025_09.s.sol b/contracts/scripts/runlogs/2025_09.s.sol new file mode 100644 index 0000000000..2a798e03bc --- /dev/null +++ b/contracts/scripts/runlogs/2025_09.s.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Setup +import { SetupBase } from "./utils/Setup.sol"; +import { SetupSonic } from "./utils/Setup.sol"; +import { SetupMainnet } from "./utils/Setup.sol"; + +import { CrossChain } from "./utils/Addresses.sol"; + +// Foundry +import { console } from "forge-std/console.sol"; + +contract Runlogs_2025_09_Mainnet is SetupMainnet { + function run() public { + //_2025_09_03(); + _2025_09_18(); + } + + // ------------------------------------------------------------------ + // September 03, 2025 - Withdraw 7 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_09_03() internal { + vm.startBroadcast(strategist); + + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 7 ether; + oethVaultAdmin.withdrawFromStrategy(address(oethWethCurveAMO), assets, amounts); + + (uint256 vaultValueAfter, uint256 totalSupplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSupplyAfter); + int256 profit = vaultChange - supplyChange; + oethVaultValueChecker.checkDelta(profit, 0.1 ether, vaultChange, 1 ether); + + console.log("-----"); + console.log("Profit : %18e", profit); + console.log("Amount withdrawn : %18e", amounts[0], "ether"); + console.log("Supply change : %18e", supplyChange); + console.log("Vault value change : %18e", vaultChange); + + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // September 18, 2025 - Deposit 86 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_09_18() internal { + vm.startBroadcast(strategist); + + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 86 ether; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + (uint256 vaultValueAfter, uint256 totalSupplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSupplyAfter); + int256 profit = vaultChange - supplyChange; + oethVaultValueChecker.checkDelta(profit, 0.1 ether, vaultChange, 1 ether); + + console.log("-----"); + console.log("Profit : %18e", profit); + console.log("Amount withdrawn : %18e", amounts[0], "ether"); + console.log("Supply change : %18e", supplyChange); + console.log("Vault value change : %18e", vaultChange); + + vm.stopBroadcast(); + } +} + +contract Runlogs_2025_09_Base is SetupBase { + function run() public { + //_2025_09_04(); + //_2025_09_09(); + //_2025_16_09(); + _2025_09_18(); + } + + // ------------------------------------------------------------------ + // September 04, 2025 - Deposit 160 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_09_04() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 160 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // September 09, 2025 - Deposit 125 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_09_09() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 125 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // September 11, 2025 - Deploy Merkl Pool Booster + Yield Forward + // ------------------------------------------------------------------ + function _2025_16_09() internal { + bytes memory campaignData = + hex"67a66cbacb2fe48ec4326932d4528215ad11656a86135f2795f5b90e501eb53800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.startBroadcast(strategist); + // Create the pool booster + poolBoosterFactoryMerkl.createPoolBoosterMerkl({ + _campaignType: 45, + _ammPoolAddress: CrossChain.MORPHO_BLUE, + _campaignDuration: 7 days, + campaignData: campaignData, + _salt: uint256(keccak256(abi.encodePacked("Merkl Pool Booster V1"))) + }); + + uint256 length = poolBoosterFactoryMerkl.poolBoosterLength(); + (address pb,,) = poolBoosterFactoryMerkl.poolBoosters(length - 1); + + // Run yield forward + oeth.undelegateYield(CrossChain.MORPHO_BLUE); + oeth.delegateYield(CrossChain.MORPHO_BLUE, address(pb)); + vm.stopBroadcast(); + } + + // ------------------------------------------------------------------ + // September 18, 2025 - Deposit 50 WETH on Curve AMO + // ------------------------------------------------------------------ + function _2025_09_18() internal { + vm.startBroadcast(strategist); + // Before + oethVaultCore.rebase(); + oethVaultValueChecker.takeSnapshot(); + + // AMO pool before + uint256 wethPoolBalanceBefore = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceBefore = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolBefore = wethPoolBalanceBefore + oethPoolBalanceBefore; + uint256 wethOutBefore = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool before"); + console.log("WETH Pool %18e", wethPoolBalanceBefore); + console.log("OETH Pool %18e", oethPoolBalanceBefore); + console.log("Total Pool %18e", totalPoolBefore); + + // Main action + uint256 amountToDeposit = 50 ether; + address[] memory assets = new address[](1); + assets[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amountToDeposit; + oethVaultAdmin.depositToStrategy(address(oethWethCurveAMO), assets, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSuplyAfter,) = + oethVaultValueChecker.snapshots(strategist); + int256 vaultChange = int256(oethVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(oeth.totalSupply()) - int256(totalSuplyAfter); + int256 profit = vaultChange - supplyChange; + + oethVaultValueChecker.checkDelta(profit, 1 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e ", profit); + console.log("OETH Supply change: %18e ", supplyChange); + console.log("Vault value change: %18e ", vaultChange); + + // AMO pool after + uint256 wethPoolBalanceAfter = weth.balanceOf(address(oethWethCurvePool)); + uint256 oethPoolBalanceAfter = oeth.balanceOf(address(oethWethCurvePool)); + uint256 totalPoolAfter = wethPoolBalanceAfter + oethPoolBalanceAfter; + uint256 wethOutAfter = oethWethCurvePool.get_dy(1, 0, 10 ether); + + console.log("-----"); + console.log("Curve OETH/WETH Pool after"); + console.log("WETH Pool %18e", wethPoolBalanceAfter); + console.log("OETH Pool %18e", oethPoolBalanceAfter); + console.log("Total Pool %18e", totalPoolAfter); + console.log( + "Sell 10 OETH Curve prices before and after: %18e || %18e", wethOutBefore, wethOutAfter + ); + vm.stopBroadcast(); + } +} + +contract Runlogs_2025_09_Sonic is SetupSonic { + function run() public { + //_2025_09_01(); + _2025_09_01_BIS(); + } + + function _2025_09_01() internal { + vm.startBroadcast(localStrategist); + address[] memory tokens = new address[](1); + tokens[0] = address(ws); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 2_644_097 ether; + + // Before + osVaultCore.rebase(); + osVaultValueChecker.takeSnapshot(); + + // Main + osVaultAdmin.depositToStrategy(address(stakingStrategy), tokens, amounts); + + // After + (uint256 vaultValueAfter, uint256 totalSupplyAfter,) = + osVaultValueChecker.snapshots(localStrategist); + int256 vaultChange = int256(osVaultCore.totalValue()) - int256(vaultValueAfter); + int256 supplyChange = int256(os.totalSupply()) - int256(totalSupplyAfter); + int256 profit = vaultChange - supplyChange; + osVaultValueChecker.checkDelta(profit, 10 ether, vaultChange, 10 ether); + + console.log("-----"); + console.log("Profit : %18e", profit); + console.log("Amount deposited : %18e", amounts[0], "ether"); + console.log("OS Supply change : %18e", supplyChange); + console.log("Vault value change: %18e", vaultChange); + + vm.stopBroadcast(); + } + + function _2025_09_01_BIS() internal { + vm.startBroadcast(timelock); + osVaultAdmin.setAssetDefaultStrategy(address(ws), address(stakingStrategy)); + vm.stopBroadcast(); + } +} diff --git a/contracts/scripts/runlogs/2025_10.s.sol b/contracts/scripts/runlogs/2025_10.s.sol new file mode 100644 index 0000000000..79a86be0a0 --- /dev/null +++ b/contracts/scripts/runlogs/2025_10.s.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Setup +import { SetupBase } from "./utils/Setup.sol"; +import { SetupSonic } from "./utils/Setup.sol"; +import { SetupMainnet } from "./utils/Setup.sol"; + +import { CrossChain } from "./utils/Addresses.sol"; + +// Foundry +import { console } from "forge-std/console.sol"; + +contract Runlogs_2025_10_Mainnet is SetupMainnet { + function run() public { + _2025_10_01(); + //_2025_10_02(); + } + + // ------------------------------------------------------------------ + // Oct 3, 2025 - Yield Forward to Computed Merkl Pool Booster + // ------------------------------------------------------------------ + function _2025_10_01() internal { + bytes memory campaignData = + hex"b8fef900b383db2dbbf4458c7f46acf5b140f26d603a6d1829963f241b82510e00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.startBroadcast(strategist); + + console.log("-----"); + console.log("strategist address", address(strategist)); + console.log("poolBoosterFactoryMerkl address", address(poolBoosterFactoryMerkl)); + + address poolBoosterAddress = poolBoosterFactoryMerkl.computePoolBoosterAddress({ + _campaignType: 45, + _ammPoolAddress: CrossChain.MORPHO_BLUE, + _campaignDuration: 7 days, + campaignData: campaignData, + _salt: uint256(keccak256(abi.encodePacked("Merkl Morpho PB OETH/USDC v1"))) + }); + + console.log("computed address", poolBoosterAddress); + + // Run yield forward + oeth.delegateYield(CrossChain.MORPHO_BLUE, poolBoosterAddress); + vm.stopBroadcast(); + } + +// ------------------------------------------------------------------ + // Oct 3+ TODO, 2025 - Create Merkl Pool Booster once Central Registry governance passes + // ------------------------------------------------------------------ + function _2025_10_02() internal { + bytes memory campaignData = + hex"b8fef900b383db2dbbf4458c7f46acf5b140f26d603a6d1829963f241b82510e00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.startBroadcast(strategist); + // Create the pool booster + poolBoosterFactoryMerkl.createPoolBoosterMerkl({ + _campaignType: 45, // Incentivise Borrow rate of OETH/USDC + _ammPoolAddress: CrossChain.MORPHO_BLUE, + _campaignDuration: 7 days, + campaignData: campaignData, + _salt: uint256(keccak256(abi.encodePacked("Merkl Morpho PB OETH/USDC v1"))) + }); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/contracts/scripts/runlogs/README.md b/contracts/scripts/runlogs/README.md new file mode 100644 index 0000000000..197260223c --- /dev/null +++ b/contracts/scripts/runlogs/README.md @@ -0,0 +1,52 @@ +# How to use foundry runlogs? + +## 1. Ensure all dependencies are installed +At the root of the repo run: +```bash +forge soldeer install +cd contract +yarn install +``` + +## 2. Execute runlogs +In the `contracts` folder run: +```bash +forge script Runlogs_2025_08_Mainnet +``` +> Note: adjust the year, month and chain accordingly. + +This generates 2 files (that are `broadcast-ready` for execution) under `contracts/broadcast`: +- run-latest.json +- run-1755456667783.json (with the timestamp corresponding to the execution time) + +## 3. Convert runlogs into Safe-compatible JSON +Since these transactions are meant to be executed from the Safe, it is not possible to use `cast`. + +To convert a `broadcast-ready` into a Safe-compatible JSON file, use the forge script: `scripts/runlogs/utils/BroadcastConvertor.sol`. + +In the `contracts` folder run: +```bash +forge script BroadcastConvertor --sig "run(string)" contracts/broadcast/2025_09.sol/146/dry-run/ +``` +> Note adjust the input accordingly: +> first the path to the run file, but stop at the dry-run folder. + +This creates, by default, a file named `run-latest-safe.json` in the same location as the input file, ready to be imported into the Safe UI. + +### Timelock targeted ? +If on the script, the address used inside `startBroadcast()` is a `Timelock`: +- the Safe-compatible JSON will be adjusted to target the `scheduleBatch` and `executeBatch` functions on the `Timelock` contract. +- two files will be generated: `run-latest-schedule` and `run-latest-execute`. + + +## 4. How generates Safe JSON in just one command? +In the `contracts` folder: +```makefile +make script +or +make script CHAIN=Mainnet +or +make script DATE=2025_08 CHAIN=Mainnet +``` + +By default it take the current year+month to fetch which runlogs to run. The default chain is Mainnet. diff --git a/contracts/scripts/runlogs/utils/Addresses.sol b/contracts/scripts/runlogs/utils/Addresses.sol new file mode 100644 index 0000000000..281dfdbe68 --- /dev/null +++ b/contracts/scripts/runlogs/utils/Addresses.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +library CrossChain { + // Governance + address public constant STRATEGIST = 0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971; + + // Protocols + address public constant MORPHO_BLUE = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; +} + +library Mainnet { + uint256 public constant CHAIN_ID = 1; // Mainnet chain ID + + // Governance + address public constant TIMELOCK = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; + + // OUSD + address public constant OUSD = 0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86; + + // OETH + address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; + address public constant WOETH = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; + address public constant OETH_VAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; + address public constant OETH_VAULT_VALUE_CHECKER = 0x31FD8618379D8e473Ec2B1540B906E8e11D2A99b; + + // OETH Strategies + address public constant OETH_WETH_CURVE_AMO = 0xba0e352AB5c13861C26e4E773e7a833C3A223FE6; + + // Pool Booster + address public constant POOL_BOOSTER_FACTORY_MERKL = 0x0FC66355B681503eFeE7741BD848080d809FD6db; + address public constant POOL_BOOSTER_CENTRAL_REGISTRY = 0xAA8af8Db4B6a827B51786334d26349eb03569731; + + // Other token + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // Curve pools + address public constant OETH_WETH_CURVE_POOL = 0xcc7d5785AD5755B6164e21495E07aDb0Ff11C2A8; +} + +library Base { + uint256 public constant CHAIN_ID = 5483; // Base chain ID + + // Governance + address public constant TIMELOCK = 0xf817cb3092179083c48c014688D98B72fB61464f; + + // OETHb + address public constant OETHB = 0xDBFeFD2e8460a6Ee4955A68582F85708BAEA60A3; + address public constant WOETHB = 0x7FcD174E80f264448ebeE8c88a7C4476AAF58Ea6; + address public constant OETHB_VAULT = 0x98a0CbeF61bD2D21435f433bE4CD42B56B38CC93; + address public constant OETHB_VAULT_VALUE_CHECKER = 0x9D98Cf85B65Fa1ACef5e9AAA2300753aDF7bcf6A; + + // OETHb Strategies + address public constant OETHB_WETH_CURVE_AMO = 0x9cfcAF81600155e01c63e4D2993A8A81A8205829; + address public constant OETHB_WETH_AERODROME_POOL = 0xF611cC500eEE7E4e4763A05FE623E2363c86d2Af; + + // Pool Booster + address public constant POOL_BOOSTER_FACTORY_MERKL = 0x1ADB902Ece465cA681C66187627a622a631a0a63; + address public constant POOL_BOOSTER_CENTRAL_REGISTRY = 0x157f0B239D7F83D153E6c95F8AD9d341694376E3; + + // Other token + address public constant WETH = 0x4200000000000000000000000000000000000006; + + // Curve pools + address public constant OETHB_WETH_CURVE_POOL = 0x302A94E3C28c290EAF2a4605FC52e11Eb915f378; +} + +library Sonic { + uint256 public constant CHAIN_ID = 146; // Sonic chain ID + + // Governance + address public constant GOVERNOR = 0xAdDEA7933Db7d83855786EB43a238111C69B00b6; + address public constant TIMELOCK = 0x31a91336414d3B955E494E7d485a6B06b55FC8fB; + address public constant STRATEGIST = 0x63cdd3072F25664eeC6FAEFf6dAeB668Ea4de94a; + + // OS + address public constant OS = 0xb1e25689D55734FD3ffFc939c4C3Eb52DFf8A794; + address public constant OS_VAULT = 0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186; + address public constant OS_VAULT_VALUE_CHECKER = 0x06f172e6852085eCa886B7f9fd8f7B21Db3D2c40; + + // ARM + address public constant ARM = 0x2F872623d1E1Af5835b08b0E49aAd2d81d649D30; + + // Strategies + address public constant STAKING_STRATEGY = 0x596B0401479f6DfE1cAF8c12838311FeE742B95c; + + // Other token + address public constant WS = 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38; +} diff --git a/contracts/scripts/runlogs/utils/BroadcastConvertor.sol b/contracts/scripts/runlogs/utils/BroadcastConvertor.sol new file mode 100644 index 0000000000..e9760c961f --- /dev/null +++ b/contracts/scripts/runlogs/utils/BroadcastConvertor.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Foundry +import { Test } from "forge-std/Test.sol"; +import { Script } from "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +// Helpers +import { Mainnet, Base, Sonic } from "./Addresses.sol"; + +/// @title Broadcast Convertor +/// @author Origin Protocol +/// @notice This contract is responsible for converting foundry broadcast messages into a format suitable for the Safe. +contract BroadcastConvertor is Script, Test { + using stdJson for string; + + // ⚠️ Never change the structure of these JSONs, alphabetical order matters!! + struct Json { + uint256 chain; + string commit; // git commit hash + string libraries; // empty + string pending; // empty + string receipts; // empty + uint256 timestamp; + Transactions[] transactions; + Returns returnData; // empty + } + + struct Transactions { + string additionalContracts; // empty + uint256 arguments; // null + address contractAddress; + string contractName; + uint256 functions; // null + uint256 hash; // null + bool isFixedGasLimit; + Transaction transaction; + string transactionType; + } + + struct Transaction { + bytes chainId; + address from; + bytes gas; + bytes input; + bytes nonce; + address to; + bytes value; + } + + struct Returns { + uint256 empty; + } + + /// @notice Main function to run the conversion from foundry broadcast messages to Safe format + /// @param path The path where the run-latest.json file is located + function run(string memory path) external { + // Fetch JSON + string memory inputJson = vm.readFile(string.concat(path, "run-latest.json")); + + // Convert Json into Json-Struct + Json memory json = abi.decode(vm.parseJson(inputJson), (Json)); + + // Is timelock targeted? + uint256 delay = timelockDelay(json); + + if (delay > 0) { + // Prepare schedule for Timelock + string memory scheduleJson = prepareForSafe(json, Timelock.scheduleBatch.selector); + console.log("\nUse this JSON to schedule on Timelock:\n%s", scheduleJson); + + // Prepare execute for Timelock + string memory executeJson = prepareForSafe(json, Timelock.executeBatch.selector); + console.log("\nUse this JSON to execute on Timelock:\n%s", executeJson); + + // Write both JSONs + vm.writeJson(scheduleJson, string.concat(path, "run-latest-schedule.json")); + vm.writeJson(executeJson, string.concat(path, "run-latest-execute.json")); + } else { + // Prepare for Safe + string memory safeJson = prepareForSafe(json, bytes4(0)); + console.log("Use this JSON is Safe:\n%s", safeJson); + + // Write Safe JSON + vm.writeJson(safeJson, string.concat(path, "run-latest-safe.json")); + } + } + + /// @notice Prepares the JSON for Safe format + /// @param json The original JSON in Json-Struct format + function prepareForSafe(Json memory json, bytes4 selector) public pure returns (string memory) { + // Header + string memory header = string.concat( + '{ "version": "1.0", "chainId": "', + vm.toString(json.chain), + '", "createdAt": ', + vm.toString(json.timestamp / 1000), // to convert milliseconds to seconds + ", " + ); + + // Meta + string memory meta = + '"meta": { "name": "Transaction Batch", "description": "", "txBuilderVersion": "1.16.1", "createdFromSafeAddress": "", "createdFromOwnerAddress": ""},'; + + // Fetch if the transaction is targeting a timelock or not + uint256 delay = timelockDelay(json); + + // Transactions + string memory transactions = string.concat( + '"transactions": [ ', + delay == 0 ? rawTransactions(json) : transactionForTimelock(json, delay, selector), + " ]," + ); + + // Is it targeting a timelock + string memory isTargetingTimelock = + string.concat('"targetingTimelock": ', delay == 0 ? "false" : "true"); + + // Building final JSON + string memory jsonObj = string.concat(header, meta, transactions, isTargetingTimelock, " }"); + + return jsonObj; + } + + /// @notice Prepares transactions for Safe format + /// @param json The original JSON in Json-Struct format + function rawTransactions(Json memory json) public pure returns (string memory transactions) { + uint256 transactionsCount = json.transactions.length; + for (uint256 i; i < transactionsCount; i++) { + // Beginning of dictionnary + transactions = string.concat(transactions, "{ "); + // to + transactions = string.concat(transactions, '"to":'); + transactions = + string.concat(transactions, '"', vm.toString(json.transactions[i].transaction.to), '",'); + // value + transactions = string.concat(transactions, '"value":'); + transactions = + string.concat(transactions, '"', vm.toString(json.transactions[i].transaction.value), '",'); + // data + transactions = string.concat(transactions, '"data": '); + transactions = + string.concat(transactions, '"', vm.toString(json.transactions[i].transaction.input), '",'); + // contractMethod & contractInputsValues + transactions = + string.concat(transactions, '"contractMethod": null, "contractInputsValues": null'); + // End of dictionary + transactions = string.concat(transactions, "}"); + + // Add comma separator if needed + if (i < transactionsCount - 1) { + transactions = string.concat(transactions, ", "); + } + } + } + + /// @notice Prepares a transaction for Safe format, when a Timelock is targeted + /// @dev This function will batch all the tx in a single one, and call the timelock pranked on the runlog + /// @param json The original JSON in Json-Struct format + /// @param delay The delay for the timelock + function transactionForTimelock(Json memory json, uint256 delay, bytes4 selector) + public + pure + returns (string memory transaction) + { + uint256 len = json.transactions.length; + address[] memory targets = new address[](len); + uint256[] memory values = new uint256[](len); + bytes[] memory payloads = new bytes[](len); + + for (uint256 i = 0; i < len; i++) { + targets[i] = json.transactions[i].transaction.to; + values[i] = uint256(bytes32(json.transactions[i].transaction.value)); + payloads[i] = json.transactions[i].transaction.input; + } + + // to + transaction = + string.concat(' { "to": "', vm.toString(json.transactions[0].transaction.from), '" '); + + // value + transaction = string.concat(transaction, ' , "value": "0" '); + + // data + transaction = string.concat( + transaction, + ' , "data": "', + selector == (Timelock.scheduleBatch.selector) + ? vm.toString(abi.encodeWithSelector(selector, targets, values, payloads, 0, 0, delay)) + : vm.toString(abi.encodeWithSelector(selector, targets, values, payloads, 0, 0)), + '",' + ); + + // contractMethod & contractInputsValues + transaction = + string.concat(transaction, ' "contractMethod": null, "contractInputsValues": null }'); + } + + /// @notice Determine if the transaction is targeting a Timelock, if so return timelock delay + /// @dev This is hardcoded instead of dynamic fetching, this allow not forking blockchain and faster runtime + /// @param json The original JSON in Json-Struct format + /// @return delay The delay for the timelock, 0 if not found + function timelockDelay(Json memory json) public pure returns (uint256 delay) { + if (json.chain == 1 && json.transactions[0].transaction.from == Mainnet.TIMELOCK) { + delay = 2 days; + } else if (json.chain == 5483 && json.transactions[0].transaction.from == Base.TIMELOCK) { + delay = 2 days; + } else if (json.chain == 146 && json.transactions[0].transaction.from == Sonic.TIMELOCK) { + delay = 2 days; + } + } +} + +interface Timelock { + function scheduleBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) external; + + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata dataElements, + bytes32 predecessor, + bytes32 salt + ) external payable; +} diff --git a/contracts/scripts/runlogs/utils/Setup.sol b/contracts/scripts/runlogs/utils/Setup.sol new file mode 100644 index 0000000000..cd09a4d079 --- /dev/null +++ b/contracts/scripts/runlogs/utils/Setup.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +// Contracts - OUSD +import { OUSD } from "contracts/contracts/token/OUSD.sol"; + +// Contracts - OETH +import { OETH } from "contracts/contracts/token/OETH.sol"; +import { WOETH } from "contracts/contracts/token/WOETH.sol"; +import { OETHBase } from "contracts/contracts/token/OETHBase.sol"; +import { WOETHBase } from "contracts/contracts/token/WOETHBase.sol"; +import { OETHVaultCore } from "contracts/contracts/vault/OETHVaultCore.sol"; +import { OETHVaultAdmin } from "contracts/contracts/vault/OETHVaultAdmin.sol"; +import { OSonicVaultCore } from "contracts/contracts/vault/OSonicVaultCore.sol"; +import { OSonicVaultAdmin } from "contracts/contracts/vault/OSonicVaultAdmin.sol"; +import { OETHBaseVaultCore } from "contracts/contracts/vault/OETHBaseVaultCore.sol"; +import { OETHBaseVaultAdmin } from "contracts/contracts/vault/OETHBaseVaultAdmin.sol"; +import { OETHVaultValueChecker } from "contracts/contracts/strategies/VaultValueChecker.sol"; + +// Contract - OS +import { OSonic } from "contracts/contracts/token/OSonic.sol"; + +// Contracts - Strategies +import { CurveAMOStrategy } from "contracts/contracts/strategies/CurveAMOStrategy.sol"; +import { BaseCurveAMOStrategy } from "contracts/contracts/strategies/BaseCurveAMOStrategy.sol"; +import { SonicStakingStrategy } from "contracts/contracts/strategies/sonic/SonicStakingStrategy.sol"; +import { AerodromeAMOStrategy } from + "contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol"; + +// Contracts - ARM +import { ISonicARM } from "contracts/contracts/interfaces/arm/ISonicARM.sol"; + +// Contracts - Pool Booster +import { PoolBoosterFactoryMerkl } from + "contracts/contracts/poolBooster/PoolBoosterFactoryMerkl.sol"; +import { PoolBoostCentralRegistry } from + "contracts/contracts/poolBooster/PoolBoostCentralRegistry.sol"; + +// Interfaces +import { IWETH9 } from "contracts/contracts/interfaces/IWETH9.sol"; +import { ICurveStableSwapNG } from "contracts/contracts/interfaces/ICurveStableSwapNG.sol"; + +// Helpers +import { CrossChain, Mainnet, Base, Sonic } from "./Addresses.sol"; + +// Foundry +import { Script } from "forge-std/Script.sol"; +import { Test } from "forge-std/Test.sol"; + +abstract contract SetupMainnet is Test, Script { + // Governance + address public strategist = CrossChain.STRATEGIST; + + // OUSD + OUSD public ousd = OUSD(Mainnet.OUSD); + + // OETH + OETH public oeth = OETH(Mainnet.OETH); + WOETH public woeth = WOETH(Mainnet.WOETH); + OETHVaultCore public oethVaultCore = OETHVaultCore(Mainnet.OETH_VAULT); + OETHVaultAdmin public oethVaultAdmin = OETHVaultAdmin(Mainnet.OETH_VAULT); + CurveAMOStrategy public oethWethCurveAMO = CurveAMOStrategy(Mainnet.OETH_WETH_CURVE_AMO); + OETHVaultValueChecker public oethVaultValueChecker = + OETHVaultValueChecker(Mainnet.OETH_VAULT_VALUE_CHECKER); + + // Pool Booster + PoolBoosterFactoryMerkl public poolBoosterFactoryMerkl = + PoolBoosterFactoryMerkl(Mainnet.POOL_BOOSTER_FACTORY_MERKL); + PoolBoostCentralRegistry public poolBoosterCentralRegistry = + PoolBoostCentralRegistry(Mainnet.POOL_BOOSTER_CENTRAL_REGISTRY); + + // Interfaces + IWETH9 public weth = IWETH9(Mainnet.WETH); + ICurveStableSwapNG public oethWethCurvePool = ICurveStableSwapNG(Mainnet.OETH_WETH_CURVE_POOL); + + function setUp() public { + // Note: to ensure perfect simulation, don't fix block number, it will be automatically set to the latest block + vm.createSelectFork(vm.envString("PROVIDER_URL")); + } +} + +abstract contract SetupBase is Test, Script { + // Governance + address public strategist = CrossChain.STRATEGIST; + + // OETH + OETHBase public oeth = OETHBase(Base.OETHB); + WOETHBase public woeth = WOETHBase(Base.WOETHB); + OETHBaseVaultCore public oethVaultCore = OETHBaseVaultCore(Base.OETHB_VAULT); + OETHBaseVaultAdmin public oethVaultAdmin = OETHBaseVaultAdmin(Base.OETHB_VAULT); + BaseCurveAMOStrategy public oethWethCurveAMO = BaseCurveAMOStrategy(Base.OETHB_WETH_CURVE_AMO); + AerodromeAMOStrategy public oethWethAerodromeAMO = + AerodromeAMOStrategy(Base.OETHB_WETH_AERODROME_POOL); + OETHVaultValueChecker public oethVaultValueChecker = + OETHVaultValueChecker(Base.OETHB_VAULT_VALUE_CHECKER); + + // Pool Booster + PoolBoosterFactoryMerkl public poolBoosterFactoryMerkl = + PoolBoosterFactoryMerkl(Base.POOL_BOOSTER_FACTORY_MERKL); + PoolBoostCentralRegistry public poolBoosterCentralRegistry = + PoolBoostCentralRegistry(Base.POOL_BOOSTER_CENTRAL_REGISTRY); + + // Interfaces + IWETH9 public weth = IWETH9(Base.WETH); + ICurveStableSwapNG public oethWethCurvePool = ICurveStableSwapNG(Base.OETHB_WETH_CURVE_POOL); + + function setUp() public { + // Note: to ensure perfect simulation, don't fix block number, it will be automatically set to the latest block + vm.createSelectFork(vm.envString("BASE_PROVIDER_URL")); + } +} + +abstract contract SetupSonic is Test, Script { + // Governance + address public governor = Sonic.GOVERNOR; + address public timelock = Sonic.TIMELOCK; + address public strategist = CrossChain.STRATEGIST; + address public localStrategist = Sonic.STRATEGIST; + + // OS + OSonic public os = OSonic(Sonic.OS); + OSonicVaultCore public osVaultCore = OSonicVaultCore(Sonic.OS_VAULT); + OSonicVaultAdmin public osVaultAdmin = OSonicVaultAdmin(Sonic.OS_VAULT); + OETHVaultValueChecker public osVaultValueChecker = + OETHVaultValueChecker(Sonic.OS_VAULT_VALUE_CHECKER); + + // Interfaces + IWETH9 public ws = IWETH9(Sonic.WS); + + // ARM + ISonicARM public arm = ISonicARM(Sonic.ARM); + + // Staking strategy + SonicStakingStrategy public stakingStrategy = + SonicStakingStrategy(payable(Sonic.STAKING_STRATEGY)); + + function setUp() public { + // Note: to ensure perfect simulation, don't fix block number, it will be automatically set to the latest block + vm.createSelectFork(vm.envString("SONIC_PROVIDER_URL")); + } +} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000000..1867a35677 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,36 @@ +[profile.default] +src = "contracts/contracts" +out = "contracts/out" +script = "contracts/scripts" +libs = ["dependencies", "contracts/node_modules"] +broadcast = "contracts/broadcast" +verbosity = 3 +auto_detect_remappings = false +viaIR = true +etherscan_api_key = "${ETHERSCAN_API_KEY}" +fs_permissions = [{ access = "read-write", path = "./"}] + +remappings = [ + "@openzeppelin/=contracts/node_modules/@openzeppelin", + "@chainlink/=contracts/node_modules/@chainlink", + "@layerzerolabs/=contracts/node_modules/@layerzerolabs", + "hardhat/=contracts/node_modules/hardhat", + "solidity-bytes-utils/=contracts/node_modules/solidity-bytes-utils/", + "forge-std/=dependencies/forge-std-1.10.0/src", +] + +[fmt] +line_length = 100 +tab_width = 2 +bracket_spacing = true + +[dependencies] +forge-std = "1.10.0" + +[soldeer] +recursive_deps = false +remappings_version = false +remappings_generate = false +remappings_regenerate = false +remappings_prefix = "@" +remappings_location = "config"