Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(SwapAndLock): lock yfi via CoveYFI instead of directly w/ YSD #297

Merged
merged 8 commits into from
Mar 28, 2024
6 changes: 4 additions & 2 deletions script/Deployments.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 17 additions & 8 deletions src/SwapAndLock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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);
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/interfaces/ISwapAndLock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/interfaces/IYearnStakingDelegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
16 changes: 6 additions & 10 deletions test/forked/SwapAndLock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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");
}
}
5 changes: 4 additions & 1 deletion test/forked/YearnStakingDelegate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/integration/Integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
27 changes: 27 additions & 0 deletions test/mocks/MockCoveYFI.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 14 additions & 2 deletions test/unit/SwapAndLock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions test/utils/YearnV3BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading