Skip to content

Commit

Permalink
add DC_sfrxETH_Burner
Browse files Browse the repository at this point in the history
  • Loading branch information
1kresh committed Jul 22, 2024
1 parent 2d7e596 commit 0adad5e
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 1 deletion.
110 changes: 110 additions & 0 deletions src/contracts/burners/DC_sfrxETH_Burner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {SelfDestruct} from "src/contracts/SelfDestruct.sol";

import {IDC_sfrxETH_Burner} from "src/interfaces/burners/DC_sfrxETH/IDC_sfrxETH_Burner.sol";
import {IFraxEtherRedemptionQueue} from "src/interfaces/burners/DC_sfrxETH/IFraxEtherRedemptionQueue.sol";

import {IDefaultCollateral} from "@symbiotic/collateral/interfaces/defaultCollateral/IDefaultCollateral.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

contract DC_sfrxETH_Burner is IDC_sfrxETH_Burner, IERC721Receiver {
using Math for uint256;
using EnumerableSet for EnumerableSet.UintSet;

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
address public immutable COLLATERAL;

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
address public immutable ASSET;

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
address public immutable FRAX_ETHER_REDEMPTION_QUEUE;

EnumerableSet.UintSet private _requestIds;

constructor(address collateral, address fraxEtherRedemptionQueue) {
COLLATERAL = collateral;

ASSET = IDefaultCollateral(collateral).asset();

FRAX_ETHER_REDEMPTION_QUEUE = fraxEtherRedemptionQueue;

IERC20(ASSET).approve(FRAX_ETHER_REDEMPTION_QUEUE, type(uint256).max);
}

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
function requestIdsLength() external view returns (uint256) {
return _requestIds.length();
}

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
function requestIds(uint256 index, uint256 maxRequestIds) external view returns (uint256[] memory requestIds_) {
uint256 length = Math.min(index + maxRequestIds, _requestIds.length()) - index;

requestIds_ = new uint256[](length);
for (uint256 i; i < length;) {
requestIds_[i] = _requestIds.at(index);
unchecked {
++i;
++index;
}
}
}

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
function triggerWithdrawal() external returns (uint256 requestId) {
uint256 amount = IERC20(COLLATERAL).balanceOf(address(this));
IDefaultCollateral(COLLATERAL).withdraw(address(this), amount);

requestId = IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).enterRedemptionQueueViaSfrxEth(
address(this), uint120(amount)
);

_requestIds.add(requestId);

emit TriggerWithdrawal(msg.sender, requestId);
}

/**
* @inheritdoc IDC_sfrxETH_Burner
*/
function triggerBurn(uint256 requestId) external {
if (!_requestIds.remove(requestId)) {
revert InvalidRequestId();
}

IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).burnRedemptionTicketNft(
requestId, payable(address(this))
);

new SelfDestruct{value: address(this).balance}();

emit TriggerBurn(msg.sender, requestId);
}

/**
* @inheritdoc IERC721Receiver
*/
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}

receive() external payable {}
}
60 changes: 60 additions & 0 deletions src/interfaces/burners/DC_sfrxETH/IDC_sfrxETH_Burner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

interface IDC_sfrxETH_Burner {
error InvalidRequestId();

/**
* @notice Emitted when a withdrawal is triggered.
* @param caller caller of the function
* @param requestId request ID that was created
*/
event TriggerWithdrawal(address indexed caller, uint256 requestId);

/**
* @notice Emitted when a burn is triggered.
* @param caller caller of the function
* @param requestId request ID of the withdrawal that was claimed and burned
*/
event TriggerBurn(address indexed caller, uint256 requestId);

/**
* @notice Get an address of the Default Collateral contract.
*/
function COLLATERAL() external view returns (address);

/**
* @notice Get an address of the collateral's asset.
*/
function ASSET() external view returns (address);

/**
* @notice Get an address of the Frax Ether Redemption queue.
*/
function FRAX_ETHER_REDEMPTION_QUEUE() external view returns (address);

/**
* @notice Get the number of unprocessed request IDs.
*/
function requestIdsLength() external view returns (uint256);

/**
* @notice Get a list of unprocessed request IDs.
* @param index index of the first request ID
* @param maxRequestIds maximum number of request IDs to return
* @return requestIds request IDs
*/
function requestIds(uint256 index, uint256 maxRequestIds) external view returns (uint256[] memory requestIds);

/**
* @notice Trigger a withdrawal of ETH from the collateral's underlying asset.
* @return requestId request ID that was created
*/
function triggerWithdrawal() external returns (uint256 requestId);

/**
* @notice Trigger a claim and a burn of ETH.
* @param requestId request ID of the withdrawal to process
*/
function triggerBurn(uint256 requestId) external;
}
30 changes: 30 additions & 0 deletions src/interfaces/burners/DC_sfrxETH/IFraxEtherRedemptionQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

interface IFraxEtherRedemptionQueue {
/// @notice State of Frax's frxETH redemption queue
/// @return nextNftId Autoincrement for the NFT id
/// @return queueLengthSecs Current wait time (in seconds) a new redeemer would have. Should be close to Beacon.
/// @return redemptionFee Redemption fee given as a percentage with 1e6 precision
/// @return earlyExitFee Early NFT back to frxETH exit fee given as a percentage with 1e6 precision
function redemptionQueueState()
external
view
returns (uint64 nextNftId, uint64 queueLengthSecs, uint64 redemptionFee, uint64 earlyExitFee);

/// @notice Enter the queue for redeeming sfrxEth to frxETH at the current rate, then frxETH to ETH 1-to-1. Must have approved or permitted first.
/// @notice Will generate a FrxETHRedemptionTicket NFT that can be redeemed for the actual ETH later.
/// @param _recipient Recipient of the NFT. Must be ERC721 compatible if a contract
/// @param _sfrxEthAmount Amount of sfrxETH to redeem (in shares / balanceOf)
/// @param _nftId The ID of the FrxEthRedemptionTicket NFT
/// @dev Must call approve/permit on frxEth contract prior to this call
function enterRedemptionQueueViaSfrxEth(
address _recipient,
uint120 _sfrxEthAmount
) external returns (uint256 _nftId);

/// @notice Redeems a FrxETHRedemptionTicket NFT for ETH. Must have reached the maturity date first.
/// @param _nftId The ID of the NFT
/// @param _recipient The recipient of the redeemed ETH
function burnRedemptionTicketNft(uint256 _nftId, address payable _recipient) external;
}
2 changes: 1 addition & 1 deletion src/interfaces/burners/DC_swETH/IDC_swETH_Burner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface IDC_swETH_Burner {
function ASSET() external view returns (address);

/**
* @notice Get an address of the Lido withdrawal queue.
* @notice Get an address of the Swell Exit contract.
*/
function SWEXIT() external view returns (address);

Expand Down
175 changes: 175 additions & 0 deletions test/burners/DC_sfrxETH_Burner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {Test, console2} from "forge-std/Test.sol";

import {DC_sfrxETH_Burner} from "src/contracts/burners/DC_sfrxETH_Burner.sol";

import {IFraxEtherRedemptionQueue} from "src/interfaces/burners/DC_sfrxETH/IFraxEtherRedemptionQueue.sol";
import {IDC_sfrxETH_Burner} from "src/interfaces/burners/DC_sfrxETH/IDC_sfrxETH_Burner.sol";

import {IERC20} from "test/mocks/AaveV3Borrow.sol";

import {IDefaultCollateral} from "@symbiotic/collateral/interfaces/defaultCollateral/IDefaultCollateral.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

contract DC_sfrxETH_BurnerTest is Test {
address owner;
address alice;
uint256 alicePrivateKey;
address bob;
uint256 bobPrivateKey;

DC_sfrxETH_Burner burner;

address public constant COLLATERAL = 0x5198CB44D7B2E993ebDDa9cAd3b9a0eAa32769D2;
address public constant FRXETH = 0x5E8422345238F34275888049021821E8E08CAa1f;
address public constant SFRXETH = 0xac3E018457B222d93114458476f3E3416Abbe38F;
address public constant FRAX_ETHER_REDEMPTION_QUEUE = 0x82bA8da44Cd5261762e629dd5c605b17715727bd;
address public constant FRXETH_MINTER = 0xbAFA44EFE7901E04E39Dad13167D089C559c1138;

function setUp() public {
// uint256 mainnetFork = vm.createFork(vm.rpcUrl("mainnet"));
// vm.selectFork(mainnetFork);

owner = address(this);
(alice, alicePrivateKey) = makeAddrAndKey("alice");
(bob, bobPrivateKey) = makeAddrAndKey("bob");

IERC20(SFRXETH).approve(COLLATERAL, type(uint256).max);

vm.deal(address(this), 1_000_000 ether);

vm.startPrank(IDefaultCollateral(COLLATERAL).limitIncreaser());
IDefaultCollateral(COLLATERAL).increaseLimit(100_000_000 ether);
vm.stopPrank();

vm.startPrank(FRXETH_MINTER);
IFrxETH(FRXETH).minter_mint(address(this), 500_000 ether);
vm.stopPrank();

IERC20(FRXETH).approve(SFRXETH, type(uint256).max);
ISfrxETH(SFRXETH).deposit(500_000 ether, address(this));

IDefaultCollateral(COLLATERAL).deposit(address(this), 400_000 ether);
}

function test_Create() public {
burner = new DC_sfrxETH_Burner(COLLATERAL, FRAX_ETHER_REDEMPTION_QUEUE);
vm.deal(address(burner), 0);

assertEq(burner.COLLATERAL(), COLLATERAL);
assertEq(burner.ASSET(), SFRXETH);
assertEq(burner.FRAX_ETHER_REDEMPTION_QUEUE(), FRAX_ETHER_REDEMPTION_QUEUE);
assertEq(IERC20(SFRXETH).allowance(address(burner), FRAX_ETHER_REDEMPTION_QUEUE), type(uint256).max);
}

function test_TriggerWithdrawal(uint256 depositAmount1, uint256 depositAmount2) public {
depositAmount1 = bound(depositAmount1, 1, 50_000 ether);
depositAmount2 = bound(depositAmount2, 1, 50_000 ether);

burner = new DC_sfrxETH_Burner(COLLATERAL, FRAX_ETHER_REDEMPTION_QUEUE);
vm.deal(address(burner), 0);

IERC20(COLLATERAL).transfer(address(burner), depositAmount1);

assertEq(IERC20(COLLATERAL).balanceOf(address(burner)), depositAmount1);
assertEq(IERC20(SFRXETH).balanceOf(address(burner)), 0);
(uint256 nextRequestId,,,) = IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).redemptionQueueState();
assertEq(address(burner).balance, 0);
uint256 requestsId = burner.triggerWithdrawal();
assertEq(address(burner).balance, 0);
assertEq(requestsId, nextRequestId);
assertEq(IERC20(COLLATERAL).balanceOf(address(burner)), 0);
assertEq(IERC20(SFRXETH).balanceOf(address(burner)), 0);

assertEq(burner.requestIdsLength(), 1);
uint256[] memory requestsIds = burner.requestIds(0, type(uint256).max);
assertEq(requestsIds.length, 1);
for (uint256 i; i < 1; ++i) {
assertEq(requestsIds[i], nextRequestId + i);
}
requestsIds = burner.requestIds(0, 0);
assertEq(requestsIds.length, 0);
requestsIds = burner.requestIds(0, 1);
assertEq(requestsIds.length, 1);
assertEq(requestsIds[0], nextRequestId);

IERC20(COLLATERAL).transfer(address(burner), depositAmount2);

assertEq(IERC20(COLLATERAL).balanceOf(address(burner)), depositAmount2);
assertEq(IERC20(SFRXETH).balanceOf(address(burner)), 0);
(nextRequestId,,,) = IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).redemptionQueueState();
assertEq(address(burner).balance, 0);
requestsId = burner.triggerWithdrawal();
assertEq(address(burner).balance, 0);
assertEq(requestsId, nextRequestId);
assertEq(IERC20(COLLATERAL).balanceOf(address(burner)), 0);
assertEq(IERC20(SFRXETH).balanceOf(address(burner)), 0);

assertEq(burner.requestIdsLength(), 2);
requestsIds = burner.requestIds(0, type(uint256).max);
assertEq(requestsIds.length, 2);
for (uint256 i; i < 2; ++i) {
assertEq(requestsIds[i], nextRequestId - 1 + i);
}
requestsIds = burner.requestIds(0, 0);
assertEq(requestsIds.length, 0);
requestsIds = burner.requestIds(0, 2);
assertEq(requestsIds.length, 2);
assertEq(requestsIds[0], nextRequestId - 1);
assertEq(requestsIds[1], nextRequestId);
}

function test_TriggerBurn(uint256 depositAmount1) public {
depositAmount1 = bound(depositAmount1, 1, 10_000 ether);

burner = new DC_sfrxETH_Burner(COLLATERAL, FRAX_ETHER_REDEMPTION_QUEUE);
vm.deal(address(burner), 0);

IERC20(COLLATERAL).transfer(address(burner), depositAmount1);

uint256 requestsId = burner.triggerWithdrawal();

vm.deal(FRAX_ETHER_REDEMPTION_QUEUE, 1_000_000 ether);

(, uint256 queueLengthSecs,,) = IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).redemptionQueueState();

vm.warp(block.timestamp + queueLengthSecs);

assertEq(address(burner).balance, 0);
burner.triggerBurn(requestsId);
assertEq(address(burner).balance, 0);

assertEq(burner.requestIdsLength(), 0);
}

function test_TriggerBurnRevertInvalidRequestId(uint256 depositAmount1) public {
depositAmount1 = bound(depositAmount1, 1, 10_000 ether);

burner = new DC_sfrxETH_Burner(COLLATERAL, FRAX_ETHER_REDEMPTION_QUEUE);
vm.deal(address(burner), 0);

IERC20(COLLATERAL).transfer(address(burner), depositAmount1);

burner.triggerWithdrawal();

vm.deal(FRAX_ETHER_REDEMPTION_QUEUE, 1_000_000 ether);

(, uint256 queueLengthSecs,,) = IFraxEtherRedemptionQueue(FRAX_ETHER_REDEMPTION_QUEUE).redemptionQueueState();

vm.warp(block.timestamp + queueLengthSecs);

vm.expectRevert(IDC_sfrxETH_Burner.InvalidRequestId.selector);
burner.triggerBurn(0);
}
}

interface IFrxETH {
// This function is what other minters will call to mint new tokens
function minter_mint(address m_address, uint256 m_amount) external;
}

interface ISfrxETH {
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
}

0 comments on commit 0adad5e

Please sign in to comment.