diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e57ffc6 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PRIVATE_KEY="" +LINEA_RPC_URL="" \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index c943a4c..f7d76ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,12 @@ [submodule "lib/periphery-contracts"] path = lib/periphery-contracts url = https://github.com/zerolend/periphery-contracts +[submodule "lib/governance"] + path = lib/governance + url = https://github.com/zerolend/governance +[submodule "lib/pyth-oracles"] + path = lib/pyth-oracles + url = https://github.com/zerolend/pyth-oracles +[submodule "lib/pyth-network-contracts"] + path = lib/pyth-network-contracts + url = https://github.com/pyth-network/pyth-sdk-solidity diff --git a/foundry.toml b/foundry.toml index 25b918f..ce63a07 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,21 @@ [profile.default] src = "src" out = "out" -libs = ["lib"] +solc = "0.8.12" +libs = [ + "lib/core-contracts", + "lib/forge-std", + "lib/periphery-contracts", + "lib/governance" +] +remappings = [ + "forge-std/=lib/forge-std/src/", + "periphery-contracts/=lib/periphery-contracts/", + "@zerolendxyz/core-v3/=lib/core-contracts/", + "@openzeppelin/=lib/core-contracts/contracts/dependencies/openzeppelin/contracts", + "@pythnetwork/pyth-sdk-solidity/=lib/pyth-network-contracts/" +] +[rpc_endpoints] +linea-mainnet = "https://linea-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/core-contracts b/lib/core-contracts index 2d518fa..cd75dfe 160000 --- a/lib/core-contracts +++ b/lib/core-contracts @@ -1 +1 @@ -Subproject commit 2d518faa63833595979adb1786a63575a94264d4 +Subproject commit cd75dfe01530fdbc5dbb5f47304b8c3658256fe8 diff --git a/lib/forge-std b/lib/forge-std index 07263d1..bf66061 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 diff --git a/lib/governance b/lib/governance new file mode 160000 index 0000000..a20868f --- /dev/null +++ b/lib/governance @@ -0,0 +1 @@ +Subproject commit a20868f7ec8163760c8b67a11d2ca0d083ed7827 diff --git a/lib/pyth-network-contracts b/lib/pyth-network-contracts new file mode 160000 index 0000000..c24b3e0 --- /dev/null +++ b/lib/pyth-network-contracts @@ -0,0 +1 @@ +Subproject commit c24b3e0173a5715c875ae035c20e063cb900f481 diff --git a/lib/pyth-oracles b/lib/pyth-oracles new file mode 160000 index 0000000..3c76628 --- /dev/null +++ b/lib/pyth-oracles @@ -0,0 +1 @@ +Subproject commit 3c766289dc0b760dfaee948d428bd101802c9841 diff --git a/script/Deploy_P001.sol b/script/Deploy_P001.s.sol similarity index 98% rename from script/Deploy_P001.sol rename to script/Deploy_P001.s.sol index 27f2452..057c76c 100644 --- a/script/Deploy_P001.sol +++ b/script/Deploy_P001.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity 0.8.12; // ███████╗███████╗██████╗ ██████╗ // ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ diff --git a/script/Deploy_P002.s.sol b/script/Deploy_P002.s.sol new file mode 100644 index 0000000..fc3678e --- /dev/null +++ b/script/Deploy_P002.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +import { P002_UpgradeIncentiveController } from "src/p002/P002_UpgradeIncentivesController.sol"; +import {Script} from "../lib/forge-std/src/Script.sol"; + +contract Deploy_P002 is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + new P002_UpgradeIncentiveController(); + + vm.stopBroadcast(); + } +} diff --git a/script/Deploy_P003.s.sol b/script/Deploy_P003.s.sol new file mode 100644 index 0000000..43ea24e --- /dev/null +++ b/script/Deploy_P003.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +import { P003_UpdateCliffForAirdrops } from "src/p003/P003_UpdateCliffForAirdrops.sol"; +import {Script} from "../lib/forge-std/src/Script.sol"; + +contract Deploy_P003 is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + new P003_UpdateCliffForAirdrops(); + + vm.stopBroadcast(); + } +} diff --git a/script/Deploy_P004.s.sol b/script/Deploy_P004.s.sol new file mode 100644 index 0000000..e49bf04 --- /dev/null +++ b/script/Deploy_P004.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +// visit https://github.com/zerolend/proposals for information about tests and deployment scripts +import { Script } from "lib/forge-std/src/Script.sol"; +import { P004_UpgradePythOracles } from "src/p004/P004_UpgradePythOracles.sol"; + +contract Deploy_P004 is Script { + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + new P004_UpgradePythOracles(); + + vm.stopBroadcast(); + } +} diff --git a/src/p001/P001_LineaIsoModeAssests.sol b/src/p001/P001_LineaIsoModeAssests.sol index 7a01a28..fc8871f 100644 --- a/src/p001/P001_LineaIsoModeAssests.sol +++ b/src/p001/P001_LineaIsoModeAssests.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.19; +pragma solidity 0.8.12; // ███████╗███████╗██████╗ ██████╗ // ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ diff --git a/src/p002/P002_UpgradeIncentivesController.sol b/src/p002/P002_UpgradeIncentivesController.sol new file mode 100644 index 0000000..a545013 --- /dev/null +++ b/src/p002/P002_UpgradeIncentivesController.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +// visit https://github.com/zerolend/proposals for information about tests and deployment scripts + +import {RewardsController} from "lib/periphery-contracts/contracts/rewards/RewardsController.sol"; + +interface IPoolAddressProvider { + function setAddressAsProxy(bytes32 id, address newImplementationAddress) external; +} + +contract P002_UpgradeIncentiveController { + address private constant EMISSIONS_MANAGER = 0x749dF84Fd6DE7c0A67db3827e5118259ed3aBBa5; + address private constant STAKING = 0x2666951A62d82860E8e1385581E2FB7669097647; + address private constant POOL_ADDRESS_PROVIDER = 0xC44827C51d00381ed4C52646aeAB45b455d200eB; + bytes32 private constant INCENTIVE_CONTROLLER_ID = 0x703c2c8634bed68d98c029c18f310e7f7ec0e5d6342c590190b3cb8b3ba54532; + + IPoolAddressProvider poolAddressProvider; + RewardsController rewardsControllerImpl; + + constructor() { + poolAddressProvider = IPoolAddressProvider(POOL_ADDRESS_PROVIDER); + rewardsControllerImpl = new RewardsController(EMISSIONS_MANAGER, STAKING); + } + + function execute() public { + poolAddressProvider.setAddressAsProxy(INCENTIVE_CONTROLLER_ID, address(rewardsControllerImpl)); + } +} diff --git a/src/p002/README.md b/src/p002/README.md new file mode 100644 index 0000000..34c46c6 --- /dev/null +++ b/src/p002/README.md @@ -0,0 +1,3 @@ +# Proposal 002 + +Proposal 002 helps in upgrading the IncentivesController proxy with the fixed boosted-rewards code, and some getters. diff --git a/src/p003/P003_UpdateCliffForAirdrops.sol b/src/p003/P003_UpdateCliffForAirdrops.sol new file mode 100644 index 0000000..aa4c1d2 --- /dev/null +++ b/src/p003/P003_UpdateCliffForAirdrops.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +// visit https://github.com/zerolend/proposals for information about tests and deployment scripts + +import { IVestedZeroNFT } from "lib/governance/contracts/interfaces/IVestedZeroNFT.sol"; +import { VestedZeroNFT } from "lib/governance/contracts/vesting/VestedZeroNFT.sol"; + +contract P003_UpdateCliffForAirdrops { + uint256 public constant LINEAR_DURATION = 86400 * 91; + uint256 public constant CLIFF_DURATION = 86400 * 90; + address public constant VESTED_ZERO_NFT_PROXY_ADDRESS = 0x9FA72ea96591e486FF065E7C8A89282dEDfA6C12; + uint256[] public tokenIds; + VestedZeroNFT public vestedZeroNFT; + + constructor() { + vestedZeroNFT = IVestedZeroNFT(VESTED_ZERO_NFT_PROXY_ADDRESS); + } + + function execute() public { + + uint256 lastTokenId = vestedZeroNFT.lastTokenId(); + + for (uint256 i = 0; i < lastTokenId; i++) { + getAirdropTokenId(i); + } + + uint256[] memory linearDuration = new uint256[](tokenIds.length); + uint256[] memory cliffDuration = new uint256[](tokenIds.length); + + for (uint256 i = 0; i < tokenIds.length; i++) { + linearDuration[i] = LINEAR_DURATION; + cliffDuration[i] = CLIFF_DURATION; + } + + vestedZeroNFT.updateCliffDuration(tokenIds, linearDuration, cliffDuration); + } + + function getAirdropTokenId(uint256 i) public { + (uint256 category,, uint256 cliffDuration) = vestedZeroNFT.tokenIdToLockDetails(i); + if (category == IVestedZeroNFT.Category.AIRDROP) { + if (cliffDuration > 86400 * 90) { + tokenIds.push(i); + } + } + } +} diff --git a/src/p003/README.md b/src/p003/README.md new file mode 100644 index 0000000..9ed45ff --- /dev/null +++ b/src/p003/README.md @@ -0,0 +1,3 @@ +# Proposal 003 + +Proposal 003 helps in updating the cliff duration from 6 months to 3 months for all airdrop vests. \ No newline at end of file diff --git a/src/p004/P004_UpgradePythOracles.sol b/src/p004/P004_UpgradePythOracles.sol new file mode 100644 index 0000000..dd91c5e --- /dev/null +++ b/src/p004/P004_UpgradePythOracles.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +// ███████╗███████╗██████╗ ██████╗ +// ╚══███╔╝██╔════╝██╔══██╗██╔═══██╗ +// ███╔╝ █████╗ ██████╔╝██║ ██║ +// ███╔╝ ██╔══╝ ██╔══██╗██║ ██║ +// ███████╗███████╗██║ ██║╚██████╔╝ +// ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ + +// Website: https://zerolend.xyz +// Discord: https://discord.gg/zerolend +// Twitter: https://twitter.com/zerolendxyz +// Telegram: https://t.me/zerolendxyz + +// visit https://github.com/zerolend/proposals for information about tests and deployment scripts +import { PythAggregatorV3 } from "lib/pyth-oracles/contracts/PythAggregatorV3.sol"; + +contract P004_UpgradePythOracles { + PythAggregatorV3 pythOracle; + + event PythOracleDeployed(address indexed deployer, address indexed pythOracleAddress); + + constructor() {} + + function execute(address pyth, bytes32 priceId, uint64 maxStalePeriod) public { + pythOracle = new PythAggregatorV3(pyth, priceId, maxStalePeriod); + emit PythOracleDeployed(msg.sender, address(pythOracle)); + } +} diff --git a/src/p004/README.md b/src/p004/README.md new file mode 100644 index 0000000..3f07744 --- /dev/null +++ b/src/p004/README.md @@ -0,0 +1,3 @@ +# Proposal P004 + +Proposal P004 helps in updating the Pyth Oracle implementation in order to avoid stale prices. \ No newline at end of file diff --git a/test/Test_P001.t.sol b/test/Test_P001.t.sol index 4588205..a55cfaf 100644 --- a/test/Test_P001.t.sol +++ b/test/Test_P001.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.12; import {Test, console} from "../lib/forge-std/src/Test.sol"; import {P001_LineaIsoModeAssests, IPoolConfigurator, IACLManager} from "../src/p001/P001_LineaIsoModeAssests.sol"; diff --git a/test/Test_P002.t.sol b/test/Test_P002.t.sol new file mode 100644 index 0000000..3df3eed --- /dev/null +++ b/test/Test_P002.t.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {P002_UpgradeIncentiveController, RewardsController} from "src/p002/P002_UpgradeIncentivesController.sol"; +import {IERC20} from "lib/core-contracts/contracts/dependencies/openzeppelin/contracts/IERC20.sol"; +interface IZeroAddressProvider { + function setAddressAsProxy(bytes32 id, address newImplementationAddress) external; +} + +interface IRewardsController { + function getAllUserRewards(address[] calldata assets, address user) external view + returns (address[] memory, uint256[] memory); + function claimAllRewards(address[] calldata assets, address to) external + returns (address[] memory, uint256[] memory); +} + +contract Test_P002 is Test { + address private constant EMISSIONS_MANAGER = 0x749dF84Fd6DE7c0A67db3827e5118259ed3aBBa5; + address private constant STAKING = 0x2666951A62d82860E8e1385581E2FB7669097647; + address private constant ZERO_ADDRESS_PROVIDER = 0xC44827C51d00381ed4C52646aeAB45b455d200eB; + address private constant ADMIN_MULTISIG = 0x14aAD4668de2115e30A5FeeE42CFa436899CCD8A; + address private constant REWARDS_CONTROLLER_PROXY = 0x28F6899fF643261Ca9766ddc251b359A2d00b945; + address private constant IMPERSONATED_USER = 0xbB226555fBB98850273B10b0CF55aD2f99966d20; + address private constant ERC20_TOKEN = 0x78354f8DcCB269a615A7e0a24f9B0718FDC3C7A7; + + IRewardsController rewardsControllerProxy; + RewardsController rewardsControllerImpl; + IERC20 rewardToken; + P002_UpgradeIncentiveController payload; + IZeroAddressProvider zeroAddressProvider; + + function setUp() public { + vm.createSelectFork(vm.envString("LINEA_RPC_URL"), 7_766_645); + + rewardsControllerProxy = IRewardsController(REWARDS_CONTROLLER_PROXY); + payload = new P002_UpgradeIncentiveController(); + + vm.startPrank(IMPERSONATED_USER); + } + + function test_P002_execute() external { + address[] memory assets = new address[](4); + uint256[] memory unclaimedAmounts; + assets[0] = 0xa2703Dc9FbACCD6eC2e4CBfa700989D0238133f6; + assets[1] = 0x476F206511a18C9956fc79726108a03E647A1817; + assets[2] = 0x0684FC172a0B8e6A65cF4684eDb2082272fe9050; + assets[3] = 0x8B6E58eA81679EeCd63468c6D4EAefA48A45868D; + + // Fetch all user rewards before claiming + (, unclaimedAmounts) = + rewardsControllerProxy.getAllUserRewards(assets, IMPERSONATED_USER); + + // Calculate the total unclaimed rewards + uint256 unclaimedRewards = 0; + for (uint256 i = 0; i < unclaimedAmounts.length; i++) { + unclaimedRewards += unclaimedAmounts[i]; + } + + // Fetch balances before + uint256 balanceBefore = rewardToken.balanceOf(IMPERSONATED_USER); + + // Claim all rewards + rewardsControllerProxy.claimAllRewards(assets, IMPERSONATED_USER); + + // Fetch all user rewards after claiming + (, unclaimedAmounts) = rewardsControllerProxy.getAllUserRewards(assets, IMPERSONATED_USER); + for (uint256 i = 0; i < unclaimedAmounts.length; i++) { + assertEq(unclaimedAmounts[i], 0); + } + + // Fetch balances after + uint256 balanceAfter = rewardToken.balanceOf(IMPERSONATED_USER); + + // Assert that the balance has increased by the unclaimed rewards + assertNotEq(balanceAfter, balanceBefore + unclaimedRewards); + + // Now we roll the fork a few blocks back, and do the same transaction. + vm.rollFork(7_766_640); + + vm.prank(ADMIN_MULTISIG); + // Upgrade the Incentive Controller + payload.execute(); + + (, unclaimedAmounts) = + rewardsControllerProxy.getAllUserRewards(assets, IMPERSONATED_USER); + + // Calculate the total unclaimed rewards + unclaimedRewards = 0; + for (uint256 i = 0; i < unclaimedAmounts.length; i++) { + unclaimedRewards += unclaimedAmounts[i]; + } + + // Fetch balances before + balanceBefore = rewardToken.balanceOf(IMPERSONATED_USER); + + // Claim all rewards + rewardsControllerProxy.claimAllRewards(assets, IMPERSONATED_USER); + + // Fetch all user rewards after claiming + (, unclaimedAmounts) = rewardsControllerProxy.getAllUserRewards(assets, IMPERSONATED_USER); + for (uint256 i = 0; i < unclaimedAmounts.length; i++) { + assertEq(unclaimedAmounts[i], 0); + } + + // Fetch balances after + balanceAfter = rewardToken.balanceOf(IMPERSONATED_USER); + + // Assert that the balance has increased by the unclaimed rewards + assertEq(balanceAfter, balanceBefore + unclaimedRewards); + } +} \ No newline at end of file diff --git a/test/Test_P003.t.sol b/test/Test_P003.t.sol new file mode 100644 index 0000000..1db0344 --- /dev/null +++ b/test/Test_P003.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.12; + +import {Test} from "lib/forge-std/src/Test.sol"; +import { P003_UpdateCliffForAirdrops, IVestedZeroNFT } from "src/p003/P003_UpdateCliffForAirdrops.sol"; + +contract Test_P003 is Test { + address private constant VESTED_ZERO_NFT_PROXY_ADDRESS = 0x9FA72ea96591e486FF065E7C8A89282dEDfA6C12; + address private constant IMPERSONATED_USER = 0xbB226555fBB98850273B10b0CF55aD2f99966d20; + uint256[] public tokenIds; + + P003_UpdateCliffForAirdrops payload; + IVestedZeroNFT vestedZeroNFT; + + function setUp() public { + vm.createSelectFor(vm.envString("LINEA_RPC_URL), 7_766_645) + + vestedZeroNFT = IVestedZeroNFT(VESTED_ZERO_NFT_PROXY_ADDRESS); + payload = new P003_UpdateCliffForAirdrops(); + + vm.startPrank(IMPERSONATED_USER); + } + + function test_P003_execute() external { + uint256 lastTokenId = vestedZeroNFT.lastTokenId(); + IVestedZeroNFT.LockDetails lockDetails; + + for (uint256 i = 0; i < lastTokenId; i++) { + payload.getAirdropTokenId(i); + } + + for (uint256 i = 0; i < tokenIds.length; i++) { + lockDetails = vestedZeroNFT.tokenIdToLockDetails(tokenIds[i]); + assertEq(lockDetails.cliffDuration, 86400 * 180); + } + + payload.execute(); + + for (uint256 i = 0; i < tokenIds.length; i++) { + lockDetails = vestedZeroNFT.tokenIdToLockDetails(tokenIds[i]); + assertEq(lockDetails.cliffDuration, 86400 * 90); + } + } +} \ No newline at end of file