-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
376 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
src/interfaces/burners/DC_sfrxETH/IFraxEtherRedemptionQueue.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |