From 637e7ed7fffc63c293709c0e502ecc29d3378ac6 Mon Sep 17 00:00:00 2001 From: "Jongseung (John) Lim" Date: Thu, 28 Mar 2024 13:13:12 -0400 Subject: [PATCH] refactor(SwapAndLock): lock yfi via CoveYFI instead of directly w/ YSD (#297) * refactor(SwapAndLock): lock yfi via CoveYFI instead of directly w/ YSD * refactor: change lockYfi function name to convertToCoveYfi * fix: deployment script params update * chore: revert unnecessary change --------- Co-authored-by: Sunil Srivatsa --- script/Deployments.s.sol | 6 ++++-- src/SwapAndLock.sol | 25 +++++++++++++++------- src/interfaces/ISwapAndLock.sol | 3 +-- src/interfaces/IYearnStakingDelegate.sol | 1 + test/forked/SwapAndLock.t.sol | 16 ++++++-------- test/forked/YearnStakingDelegate.t.sol | 5 ++++- test/integration/Integration.t.sol | 2 +- test/mocks/MockCoveYFI.sol | 27 ++++++++++++++++++++++++ test/unit/SwapAndLock.t.sol | 16 ++++++++++++-- test/utils/YearnV3BaseTest.t.sol | 4 ++-- 10 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 test/mocks/MockCoveYFI.sol diff --git a/script/Deployments.s.sol b/script/Deployments.s.sol index 24db3414..ce14acd8 100644 --- a/script/Deployments.s.sol +++ b/script/Deployments.s.sol @@ -142,9 +142,11 @@ contract Deployments is BaseDeployScript, SablierBatchCreator, CurveSwapParamsCo "StakingDelegateRewards", MAINNET_DYFI, address(ysd), admin, timeLock, options ) ); - address swapAndLock = address(deployer.deploy_SwapAndLock("SwapAndLock", address(ysd), broadcaster, options)); deployer.deploy_DYFIRedeemer("DYFIRedeemer", admin, options); - deployer.deploy_CoveYFI("CoveYFI", address(ysd), admin, options); + address coveYfi = address(deployer.deploy_CoveYFI("CoveYFI", address(ysd), admin, options)); + address swapAndLock = + address(deployer.deploy_SwapAndLock("SwapAndLock", address(ysd), coveYfi, broadcaster, options)); + // Admin transactions SwapAndLock(swapAndLock).setDYfiRedeemer(deployer.getAddress("DYFIRedeemer")); ysd.setSwapAndLock(swapAndLock); diff --git a/src/SwapAndLock.sol b/src/SwapAndLock.sol index 80861a12..9d7fbdda 100644 --- a/src/SwapAndLock.sol +++ b/src/SwapAndLock.sol @@ -7,6 +7,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IYearnStakingDelegate, IVotingYFI } from "src/interfaces/IYearnStakingDelegate.sol"; import { ISwapAndLock } from "src/interfaces/ISwapAndLock.sol"; +import { CoveYFI } from "./CoveYFI.sol"; /** * @title SwapAndLock @@ -25,9 +26,12 @@ contract SwapAndLock is ISwapAndLock, AccessControlEnumerable { address private constant _D_YFI = 0x41252E8691e964f7DE35156B68493bAb6797a275; // Immutables + // slither-disable-start naming-convention /// @dev Address of the YearnStakingDelegate contract, set at deployment and immutable thereafter. - // slither-disable-next-line naming-convention address private immutable _YEARN_STAKING_DELEGATE; + /// @dev Address of the CoveYFI contract, set at deployment and immutable thereafter. + address private immutable _COVE_YFI; + // slither-disable-end naming-convention /// @notice Address of the DYfiRedeemer contract. address private _dYfiRedeemer; @@ -42,26 +46,31 @@ contract SwapAndLock is ISwapAndLock, AccessControlEnumerable { /** * @notice Constructs the SwapAndLock contract. * @param yearnStakingDelegate_ Address of the YearnStakingDelegate contract. + * @param coveYfi_ Address of the CoveYFI contract. + * @param admin Address of the contract admin for rescuing tokens. */ // slither-disable-next-line locked-ether - constructor(address yearnStakingDelegate_, address admin) payable { + constructor(address yearnStakingDelegate_, address coveYfi_, address admin) payable { // Checks - if (yearnStakingDelegate_ == address(0)) { + if (coveYfi_ == address(0) || yearnStakingDelegate_ == address(0)) { revert Errors.ZeroAddress(); } // Effects _YEARN_STAKING_DELEGATE = yearnStakingDelegate_; + _COVE_YFI = coveYfi_; _grantRole(DEFAULT_ADMIN_ROLE, admin); // Interactions - IERC20(_YFI).forceApprove(yearnStakingDelegate_, type(uint256).max); + IERC20(_YFI).forceApprove(coveYfi_, type(uint256).max); } /** - * @notice Locks YFI in the YearnStakingDelegate contract. - * @return The total amount of YFI locked and the end timestamp of the lock after the lock operation. + * @notice Converts any YFI held by this contract to CoveYFI, minting CoveYFI to the treasury. YFI will be locked as + * veYFI under YearnStakingDelegate's ownership. + * @return The amount of coveYFI minted. */ - function lockYfi() external returns (IVotingYFI.LockedBalance memory) { - return IYearnStakingDelegate(_YEARN_STAKING_DELEGATE).lockYfi(IERC20(_YFI).balanceOf(address(this))); + function convertToCoveYfi() external returns (uint256) { + address treasury = IYearnStakingDelegate(_YEARN_STAKING_DELEGATE).treasury(); + return CoveYFI(_COVE_YFI).deposit(IERC20(_YFI).balanceOf(address(this)), treasury); } /** diff --git a/src/interfaces/ISwapAndLock.sol b/src/interfaces/ISwapAndLock.sol index 6394a2f6..55c2391c 100644 --- a/src/interfaces/ISwapAndLock.sol +++ b/src/interfaces/ISwapAndLock.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.18; import { IAccessControlEnumerable } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol"; -import { IVotingYFI } from "src/interfaces/deps/yearn/veYFI/IVotingYFI.sol"; interface ISwapAndLock is IAccessControlEnumerable { - function lockYfi() external returns (IVotingYFI.LockedBalance memory); + function convertToCoveYfi() external returns (uint256); function setDYfiRedeemer(address newDYfiRedeemer) external; function dYfiRedeemer() external view returns (address); } diff --git a/src/interfaces/IYearnStakingDelegate.sol b/src/interfaces/IYearnStakingDelegate.sol index 8df1aa63..8de51810 100644 --- a/src/interfaces/IYearnStakingDelegate.sol +++ b/src/interfaces/IYearnStakingDelegate.sol @@ -49,4 +49,5 @@ interface IYearnStakingDelegate { function getGaugeRewardSplit(address gauge) external view returns (RewardSplit memory); function getBoostRewardSplit() external view returns (BoostRewardSplit memory); function getExitRewardSplit() external view returns (ExitRewardSplit memory); + function treasury() external view returns (address); } diff --git a/test/forked/SwapAndLock.t.sol b/test/forked/SwapAndLock.t.sol index 6500ed3a..7da14705 100644 --- a/test/forked/SwapAndLock.t.sol +++ b/test/forked/SwapAndLock.t.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.18; import { YearnV3BaseTest } from "test/utils/YearnV3BaseTest.t.sol"; import { ISwapAndLock } from "src/interfaces/ISwapAndLock.sol"; -import { IVotingYFI } from "src/interfaces/deps/yearn/veYFI/IVotingYFI.sol"; +import { CoveYFI } from "src/CoveYFI.sol"; import { IYearnStakingDelegate } from "src/interfaces/IYearnStakingDelegate.sol"; import { IDYFIRedeemer } from "src/interfaces/IDYFIRedeemer.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract SwapAndLock_ForkedTest is YearnV3BaseTest { address public yearnStakingDelegate; + address public coveYfi; address public swapAndLock; address public dYfiRedeemer; @@ -20,7 +21,8 @@ contract SwapAndLock_ForkedTest is YearnV3BaseTest { redeemCaller = createUser("redeemCaller"); address receiver = setUpGaugeRewardReceiverImplementation(admin); yearnStakingDelegate = setUpYearnStakingDelegate(receiver, admin, admin, admin, admin); - swapAndLock = setUpSwapAndLock(admin, yearnStakingDelegate); + coveYfi = address(new CoveYFI(yearnStakingDelegate, admin)); + swapAndLock = setUpSwapAndLock(admin, yearnStakingDelegate, coveYfi); dYfiRedeemer = setUpDYfiRedeemer(admin); vm.startPrank(admin); IYearnStakingDelegate(yearnStakingDelegate).setSwapAndLock(swapAndLock); @@ -49,13 +51,7 @@ contract SwapAndLock_ForkedTest is YearnV3BaseTest { // Check for the new veYFI balance vm.prank(admin); - IVotingYFI.LockedBalance memory lockedBalance = ISwapAndLock(swapAndLock).lockYfi(); - assertApproxEqRel(lockedBalance.amount, yfiAmount, 0.001e18, "lockYfi failed: locked amount is incorrect"); - assertApproxEqRel( - lockedBalance.end, - block.timestamp + 4 * 365 days + 4 weeks, - 0.001e18, - "lockYfi failed: locked end timestamp is incorrect" - ); + uint256 coveYfiMinted = ISwapAndLock(swapAndLock).convertToCoveYfi(); + assertEq(coveYfiMinted, yfiAmount, "Incorrect coveYFI mint"); } } diff --git a/test/forked/YearnStakingDelegate.t.sol b/test/forked/YearnStakingDelegate.t.sol index 518c7504..7c2c0bf2 100644 --- a/test/forked/YearnStakingDelegate.t.sol +++ b/test/forked/YearnStakingDelegate.t.sol @@ -10,11 +10,13 @@ import { IYearnStakingDelegate, YearnStakingDelegate } from "src/YearnStakingDel import { Errors } from "src/libraries/Errors.sol"; import { IGauge } from "src/interfaces/deps/yearn/veYFI/IGauge.sol"; import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import { CoveYFI } from "src/CoveYFI.sol"; contract YearnStakingDelegate_ForkedTest is YearnV3BaseTest { using SafeERC20 for IERC20; YearnStakingDelegate public yearnStakingDelegate; + address public coveYfi; address public gauge; address public vault; address public stakingDelegateRewards; @@ -59,7 +61,8 @@ contract YearnStakingDelegate_ForkedTest is YearnV3BaseTest { address receiver = setUpGaugeRewardReceiverImplementation(admin); yearnStakingDelegate = new YearnStakingDelegate(receiver, treasury, admin, pauser, timelock); stakingDelegateRewards = setUpStakingDelegateRewards(admin, MAINNET_DYFI, address(yearnStakingDelegate)); - swapAndLock = setUpSwapAndLock(admin, address(yearnStakingDelegate)); + coveYfi = address(new CoveYFI(address(yearnStakingDelegate), admin)); + swapAndLock = setUpSwapAndLock(admin, address(yearnStakingDelegate), coveYfi); // Setup approvals for YFI spending vm.startPrank(alice); diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index d01facee..390790c7 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -82,11 +82,11 @@ contract YearnGaugeStrategy_IntegrationTest is YearnV3BaseTest { vm.label(yearnStakingDelegate.gaugeRewardReceivers(gauge), "gaugeRewardReceiver"); stakingDelegateRewards = StakingDelegateRewards(setUpStakingDelegateRewards(admin, MAINNET_DYFI, address(yearnStakingDelegate))); - swapAndLock = SwapAndLock(setUpSwapAndLock(admin, address(yearnStakingDelegate))); dYfiRedeemer = new DYFIRedeemer(admin); vm.label(address(dYfiRedeemer), "dYfiRedeemer"); coveYfi = new CoveYFI(address(yearnStakingDelegate), admin); vm.label(address(coveYfi), "coveYfi"); + swapAndLock = SwapAndLock(setUpSwapAndLock(admin, address(yearnStakingDelegate), address(coveYfi))); coveYfiRewardsGauge = ERC20RewardsGauge(_cloneContract(erc20RewardsGaugeImplementation)); coveYfiRewardsGauge.initialize(address(coveYfi)); coveYfiRewardForwarder = RewardForwarder(_cloneContract(rewardForwarderImplementation)); diff --git a/test/mocks/MockCoveYFI.sol b/test/mocks/MockCoveYFI.sol new file mode 100644 index 00000000..73a8223a --- /dev/null +++ b/test/mocks/MockCoveYFI.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +contract MockCoveYFI is ERC20Mock { + address private immutable _YFI; + + constructor(address yfi) ERC20Mock() { + _YFI = yfi; + } + + function deposit(uint256 balance) external returns (uint256) { + return _deposit(balance, msg.sender); + } + + function deposit(uint256 balance, address receiver) external returns (uint256) { + return _deposit(balance, receiver); + } + + function _deposit(uint256 balance, address receiver) internal returns (uint256) { + _mint(receiver, balance); + IERC20(_YFI).transferFrom(msg.sender, address(this), balance); + return balance; + } +} diff --git a/test/unit/SwapAndLock.t.sol b/test/unit/SwapAndLock.t.sol index 21fad2fb..0858293e 100644 --- a/test/unit/SwapAndLock.t.sol +++ b/test/unit/SwapAndLock.t.sol @@ -8,19 +8,24 @@ import { MockYearnStakingDelegate } from "test/mocks/MockYearnStakingDelegate.so import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; import { Errors } from "src/libraries/Errors.sol"; +import { IYearnStakingDelegate } from "src/interfaces/IYearnStakingDelegate.sol"; +import { MockCoveYFI } from "test/mocks/MockCoveYFI.sol"; contract SwapAndLock_Test is BaseTest { address public yearnStakingDelegate; address public swapAndLock; address public admin; + address public treasury; address public yfi; address public dYfi; + address public coveYfi; event DYfiRedeemerSet(address oldRedeemer, address newRedeemer); function setUp() public override { super.setUp(); admin = createUser("admin"); + treasury = createUser("treasury"); // Deploy mock tokens yfi = MAINNET_YFI; @@ -30,17 +35,24 @@ contract SwapAndLock_Test is BaseTest { // Deploy mock contracts to be called in SwapAndLock yearnStakingDelegate = address(new MockYearnStakingDelegate()); + coveYfi = address(new MockCoveYFI(yfi)); // Deploy SwapAndLock - swapAndLock = address(new SwapAndLock(yearnStakingDelegate, admin)); + swapAndLock = address(new SwapAndLock(yearnStakingDelegate, coveYfi, admin)); + + // Mock yearnStakingDelegate.treasury() + vm.mockCall( + yearnStakingDelegate, abi.encodeWithSelector(IYearnStakingDelegate.treasury.selector), abi.encode(treasury) + ); } function test_lockYfi() public { uint256 yfiAmount = 10e18; airdrop(IERC20(yfi), swapAndLock, yfiAmount); vm.prank(admin); - ISwapAndLock(swapAndLock).lockYfi(); + ISwapAndLock(swapAndLock).convertToCoveYfi(); assertEq(IERC20(yfi).balanceOf(address(swapAndLock)), 0); + assertEq(IERC20(coveYfi).balanceOf(address(treasury)), yfiAmount); } function testFuzz_setDYfiRedeemer(address a) public { diff --git a/test/utils/YearnV3BaseTest.t.sol b/test/utils/YearnV3BaseTest.t.sol index 0d99009a..2ba33ed5 100644 --- a/test/utils/YearnV3BaseTest.t.sol +++ b/test/utils/YearnV3BaseTest.t.sol @@ -172,8 +172,8 @@ contract YearnV3BaseTest is BaseTest { return gaugeRewardReceiverImplementation; } - function setUpSwapAndLock(address owner, address yearnStakingDelegate) public returns (address) { - address swapAndLock = address(new SwapAndLock(yearnStakingDelegate, owner)); + function setUpSwapAndLock(address owner, address yearnStakingDelegate, address coveYfi) public returns (address) { + address swapAndLock = address(new SwapAndLock(yearnStakingDelegate, coveYfi, owner)); vm.label(swapAndLock, "SwapAndLock"); return swapAndLock; }