diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..80956bb1 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +name: CI + +on: [push] + +jobs: + foundry-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Install forge-std + run: | + forge install foundry-rs/forge-std --no-commit + forge remappings > remappings.txt + - name: Run non-Kontrol tests + run: | + forge test --no-match-path "src/test/kontrol/*" -vvv + - name: Run snapshot without Kontrol tests + run: forge snapshot --no-match-path "src/test/kontrol/*" + + \ No newline at end of file diff --git a/.github/workflows/deploy-sepolia-governance.yaml b/.github/workflows/deploy-sepolia-governance.yaml new file mode 100644 index 00000000..11ec0be5 --- /dev/null +++ b/.github/workflows/deploy-sepolia-governance.yaml @@ -0,0 +1,50 @@ +name: "[sepolia-deploy] deploy governance for strategy" +on: + workflow_dispatch: + inputs: + proposer: + description: 'Proposer address' + required: true + default: '0x' + strategy: + description: 'Strategy Address' + required: true + default: '0x' + governorRoleAddress: + description: 'Governor role address' + required: true + default: '0x' + governorVaults: + description: 'Governor vaults as comma separated' + required: false + default: '' + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: sepolia + url: https://term-finance.github.io/yearn-v3-term-vault/ + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + - run: forge install + - run: forge build + - run: forge tree + - run: forge script script/DeployGovernance.s.sol:DeployGovernance --rpc-url $RPC_URL --broadcast --gas-price 500000000000 --verify --verbosity 4 + env: + RPC_URL: ${{ secrets.RPC_URL }} + PRIVATE_KEY: ${{ secrets.GOVERNANCE_DEPLOYER_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + PROPOSER: ${{ github.event.inputs.proposer }} + STRATEGY: ${{ github.event.inputs.strategy }} + GOVERNOR: ${{ github.event.inputs.governorRoleAddress }} + VAULT_GOVERNORS: ${{ github.event.inputs.governorVaults }} + GOVERNANCE_FACTORY: ${{ vars.GOVERNANCE_FACTORY }} + + + + \ No newline at end of file diff --git a/.github/workflows/deploy-sepolia-strategy.yaml b/.github/workflows/deploy-sepolia-strategy.yaml index b4a86d9f..849adb30 100644 --- a/.github/workflows/deploy-sepolia-strategy.yaml +++ b/.github/workflows/deploy-sepolia-strategy.yaml @@ -10,13 +10,9 @@ on: description: 'Yearn strategy name' required: true default: '0x' - governorRoleAddress: - description: 'Governor role address' - required: true - default: '0x' strategyManagementAddress: description: 'Strategy management address' - required: false + required: true default: '0x' discountRateMarkup: description: 'Discount rate markup' @@ -34,6 +30,18 @@ on: description: 'Required reserve ratio' required: false default: '0.01' + profitMaxUnlock: + description: 'Profit max unlock time' + required: false + default: '0' + collateralTokens: + description: 'Collateral tokens comma separated' + required: false + default: '0x' + minCollateralRatios: + description: 'Minimum collateral ratio comma separated' + required: false + default: '0.01' jobs: deploy: @@ -54,6 +62,7 @@ jobs: env: RPC_URL: ${{ secrets.RPC_URL }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + GOVERNOR_DEPLOYER_KEY: ${{ secrets.GOVERNANCE_DEPLOYER_KEY }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} ASSET_ADDRESS: ${{ github.event.inputs.asset }} YEARN_VAULT_ADDRESS: ${{ vars.YEARN_VAULT_ADDRESS }} @@ -66,7 +75,10 @@ jobs: REPOTOKEN_CONCENTRATION_LIMIT: ${{ github.event.inputs.repoTokenConcentrationLimit }} ADMIN_ADDRESS: ${{ vars.ADMIN_ADDRESS }} DEVOPS_ADDRESS: ${{ vars.DEVOPS_ADDRESS }} - GOVERNOR_ROLE_ADDRESS: ${{ github.event.inputs.governorRoleAddress }} + GOVERNOR_ROLE_ADDRESS: ${{ vars.GOVERNANCE_FACTORY }} STRATEGY_MANAGEMENT_ADDRESS: ${{ github.event.inputs.strategyManagementAddress }} NEW_REQUIRED_RESERVE_RATIO: ${{ github.event.inputs.requiredReserveRatio }} + COLLATERAL_TOKEN_ADDRESSES: ${{ github.event.inputs.collateralTokens }} + MIN_COLLATERAL_RATIOS: ${{ github.event.inputs.minCollateralRatios }} + PROFIT_MAX_UNLOCK_TIME: ${{ github.event.inputs.profitMaxUnlock }} \ No newline at end of file diff --git a/.github/workflows/stylebot.yaml b/.github/workflows/stylebot.yaml new file mode 100644 index 00000000..dcaac271 --- /dev/null +++ b/.github/workflows/stylebot.yaml @@ -0,0 +1,67 @@ +name: stylebot +on: + push: + branches: + - master +concurrency: + group: "stylebot" + cancel-in-progress: true +permissions: + id-token: write + contents: write + packages: write + pull-requests: write +jobs: + fix: + runs-on: ubuntu-latest + + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.STYLEBOT_GITHUB_APP_ID }} + private-key: ${{ secrets.STYLEBOT_GITHUB_APP_KEY }} + - uses: actions/checkout@master + - id: nodeversion + run: echo "version=$(grep nodejs .tool-versions | sed -e 's/[^[:space:]]*[[:space:]]*//')" >> $GITHUB_OUTPUT + - uses: actions/setup-node@v4 + with: + node-version: ${{ steps.nodeversion.outputs.version }} + cache: yarn + - run: yarn install --immutable + # Run fixes, save stdout. + - run: | + echo 'ESLINT_RESULTS<> ${GITHUB_ENV} + yarn fix:eslint >> ${GITHUB_ENV} || true + echo 'EOF' >> ${GITHUB_ENV} + - run: | + echo 'PRETTIER_RESULTS<> ${GITHUB_ENV} + yarn fix:prettier >> ${GITHUB_ENV} + echo 'EOF' >> ${GITHUB_ENV} + - run: git restore .yarn .yarnrc.yml + # Make PR from local changes. + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "[stylebot] Fixes for code style" + branch: stylebot/patch + title: "[stylebot] Fixes for code style" + body: | + Stylebot detected automatically fix-able code style issues. + +
`yarn fix:eslint` + + ``` + ${{ env.ESLINT_RESULTS }} + ``` + +
+ +
`yarn fix:prettier` + + ``` + ${{ env.PRETTIER_RESULTS }} + ``` + +
diff --git a/foundry.toml b/foundry.toml index d6ff1518..984c28da 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,4 +1,5 @@ [profile.default] +evm_version = 'shanghai' src = 'src' out = 'out' libs = ['lib'] diff --git a/lib/forge-std b/lib/forge-std index fc560fa3..0e709775 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit fc560fa34fa12a335a50c35d92e55a6628ca467c +Subproject commit 0e7097750918380d84dd3cfdef595bee74dabb70 diff --git a/script/DeployGovernance.s.sol b/script/DeployGovernance.s.sol new file mode 100644 index 00000000..3a83580f --- /dev/null +++ b/script/DeployGovernance.s.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; +import "forge-std/Script.sol"; +import "../src/Strategy.sol"; + +interface TermVaultGovernanceFactory { + function deploySafe( + address proposer, + address strategy, + address governor, + address[] calldata vaultGovernors + ) external; +} + +contract DeployGovernance is Script { + /** + * @dev Converts a comma-separated string of addresses to an array of addresses. + * @param _input A string containing comma-separated addresses. + * @return addressArray An array of addresses parsed from the input string. + */ + function stringToAddressArray(string memory _input) public pure returns (address[] memory) { + // Check if the input string is empty + if (bytes(_input).length == 0) { + return new address[](0); + } + // Step 1: Split the input string by commas + string[] memory parts = splitString(_input, ","); + + // Step 2: Convert each part to an address + address[] memory addressArray = new address[](parts.length); + for (uint256 i = 0; i < parts.length; i++) { + addressArray[i] = parseAddress(parts[i]); + } + + return addressArray; + } + + + /** + * @dev Helper function to split a string by a delimiter + * @param _str The input string + * @param _delimiter The delimiter to split by + * @return An array of substrings + */ + function splitString(string memory _str, string memory _delimiter) internal pure returns (string[] memory) { + bytes memory strBytes = bytes(_str); + bytes memory delimiterBytes = bytes(_delimiter); + uint256 partsCount = 1; + + // Count the parts to split the string + for (uint256 i = 0; i < strBytes.length - 1; i++) { + if (strBytes[i] == delimiterBytes[0]) { + partsCount++; + } + } + + string[] memory parts = new string[](partsCount); + uint256 partIndex = 0; + bytes memory part; + + for (uint256 i = 0; i < strBytes.length; i++) { + if (strBytes[i] == delimiterBytes[0]) { + parts[partIndex] = string(part); + part = ""; + partIndex++; + } else { + part = abi.encodePacked(part, strBytes[i]); + } + } + + // Add the last part + parts[partIndex] = string(part); + + return parts; + } + + /** + * @dev Helper function to parse a string and convert it to an address + * @param _str The string representation of an address + * @return The address parsed from the input string + */ + function parseAddress(string memory _str) internal pure returns (address) { + bytes memory tmp = bytes(_str); + require(tmp.length == 42, "Invalid address length"); // Must be 42 characters long (0x + 40 hex chars) + + uint160 addr = 0; + for (uint256 i = 2; i < 42; i++) { + uint160 b = uint160(uint8(tmp[i])); + + if (b >= 48 && b <= 57) { // 0-9 + addr = addr * 16 + (b - 48); + } else if (b >= 65 && b <= 70) { // A-F + addr = addr * 16 + (b - 55); + } else if (b >= 97 && b <= 102) { // a-f + addr = addr * 16 + (b - 87); + } else { + revert("Invalid address character"); + } + } + + return address(addr); + } + + function run() external { + uint256 deployerPK = vm.envUint("PRIVATE_KEY"); + + // Set up the RPC URL (optional if you're using the default foundry config) + string memory rpcUrl = vm.envString("RPC_URL"); + + vm.startBroadcast(deployerPK); + + TermVaultGovernanceFactory factory = TermVaultGovernanceFactory(vm.envAddress("GOVERNANCE_FACTORY")); + address proposer = vm.envAddress("PROPOSER"); + address strategy = vm.envAddress("STRATEGY"); + address governor = vm.envAddress("GOVERNOR"); + address[] memory vaultGovernors = stringToAddressArray(vm.envString("VAULT_GOVERNORS")); + + factory.deploySafe(proposer, strategy, governor, vaultGovernors); + + vm.stopBroadcast(); + } + +} \ No newline at end of file diff --git a/script/Strategy.s.sol b/script/Strategy.s.sol index 3d6e7c85..56eba271 100644 --- a/script/Strategy.s.sol +++ b/script/Strategy.s.sol @@ -14,6 +14,10 @@ contract DeployStrategy is Script { * @return addressArray An array of addresses parsed from the input string. */ function stringToAddressArray(string memory _input) public pure returns (address[] memory) { + // Check if the input string is empty + if (bytes(_input).length == 0) { + return new address[](0); + } // Step 1: Split the input string by commas string[] memory parts = splitString(_input, ","); @@ -32,6 +36,10 @@ contract DeployStrategy is Script { * @return uintArray An array of uint256 parsed from the input string. */ function stringToUintArray(string memory _input) public pure returns (uint256[] memory) { + // Check if the input string is empty + if (bytes(_input).length == 0) { + return new uint256[](0); + } // Step 1: Split the input string by commas string[] memory parts = splitString(_input, ","); @@ -137,12 +145,19 @@ contract DeployStrategy is Script { // Retrieve environment variables string memory name = vm.envString("STRATEGY_NAME"); address strategyManagement = vm.envAddress("STRATEGY_MANAGEMENT_ADDRESS"); + address[] memory collateralTokens = stringToAddressArray(vm.envString("COLLATERAL_TOKEN_ADDRESSES")); + uint256[] memory minCollateralRatios = stringToUintArray(vm.envString("MIN_COLLATERAL_RATIOS")); + address governorRoleAddress = vm.envAddress("GOVERNOR_ROLE_ADDRESS"); + uint256 profitMaxUnlockTime = vm.envUint("PROFIT_MAX_UNLOCK_TIME"); + bool isTest = vm.envBool("IS_TEST"); TermVaultEventEmitter eventEmitter = _deployEventEmitter(); - Strategy.StrategyParams memory params = buildStrategyParams(address(eventEmitter)); + address deployer = vm.addr(deployerPK); + + Strategy.StrategyParams memory params = buildStrategyParams(address(eventEmitter), deployer); Strategy strategy = new Strategy( name, @@ -152,24 +167,33 @@ contract DeployStrategy is Script { console.log("deployed strategy contract to"); console.log(address(strategy)); + ITokenizedStrategy(address(strategy)).setProfitMaxUnlockTime(profitMaxUnlockTime); + ITokenizedStrategy(address(strategy)).setPendingManagement(strategyManagement); console.log("set pending management"); console.log(strategyManagement); - if (isTest) { eventEmitter.pairVaultContract(address(strategy)); console.log("paired strategy contract with event emitter"); } + + for (uint256 i = 0; i < collateralTokens.length; i++) { + strategy.setCollateralTokenParams(collateralTokens[i], minCollateralRatios[i]); + } + + + strategy.setPendingGovernor(governorRoleAddress); + console.log("set pending governor"); + console.log(governorRoleAddress); vm.stopBroadcast(); } - function buildStrategyParams(address eventEmitter) internal returns(Strategy.StrategyParams memory) { + function buildStrategyParams(address eventEmitter, address deployer) internal returns(Strategy.StrategyParams memory) { address asset = vm.envAddress("ASSET_ADDRESS"); address yearnVaultAddress = vm.envAddress("YEARN_VAULT_ADDRESS"); address discountRateAdapterAddress = vm.envAddress("DISCOUNT_RATE_ADAPTER_ADDRESS"); - address governorRoleAddress = vm.envAddress("GOVERNOR_ROLE_ADDRESS"); address termController = vm.envAddress("TERM_CONTROLLER_ADDRESS"); uint256 discountRateMarkup = vm.envUint("DISCOUNT_RATE_MARKUP"); uint256 timeToMaturityThreshold = vm.envUint("TIME_TO_MATURITY_THRESHOLD"); @@ -183,7 +207,7 @@ contract DeployStrategy is Script { yearnVaultAddress, discountRateAdapterAddress, address(eventEmitter), - governorRoleAddress, + deployer, termController, repoTokenConcentrationLimit, timeToMaturityThreshold, diff --git a/src/Strategy.sol b/src/Strategy.sol index b5f87785..8ddb639c 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -162,8 +162,8 @@ contract Strategy is BaseStrategy, Pausable, AccessControl { _revokeRole(GOVERNOR_ROLE, strategyState.governorAddress); _grantRole(GOVERNOR_ROLE, pendingGovernor); strategyState.governorAddress = pendingGovernor; - pendingGovernor = address(0); TERM_VAULT_EVENT_EMITTER.emitNewGovernor(pendingGovernor); + pendingGovernor = address(0); } /**