Skip to content

Commit

Permalink
refactor(SwapAndLock): lock yfi via CoveYFI instead of directly w/ YSD (
Browse files Browse the repository at this point in the history
#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 <[email protected]>
  • Loading branch information
penandlim and alphastorm authored Mar 28, 2024
1 parent 08dd75f commit 637e7ed
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 28 deletions.
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

0 comments on commit 637e7ed

Please sign in to comment.