From 4156d3f5f21e8d78cdd5de418566a2f16d626e4c Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Thu, 29 Sep 2022 16:37:42 -0700 Subject: [PATCH 1/7] test not working --- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- src/IsfrxETH.sol | 1 - src/frxETHMinter.sol | 7 +- test/frxETHMinter.t.sol | 18 ++--- test/frxETH_sfrxETH_combo.t.sol | 112 -------------------------------- 6 files changed, 16 insertions(+), 126 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index 340fe86..cb69e9c 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 340fe86d25dc6424fa04cf2b05447fccc71ee86a +Subproject commit cb69e9c07fbd002819c8c6c8db3caeab76b90d6b diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 7a14f6c..26dddee 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 7a14f6c5953a1f2228280e6eb1dfee8e5c28d79a +Subproject commit 26dddee1c05ff91264ae24c5c172bd05827cf5d8 diff --git a/src/IsfrxETH.sol b/src/IsfrxETH.sol index 41e1e1c..ef6ece6 100644 --- a/src/IsfrxETH.sol +++ b/src/IsfrxETH.sol @@ -20,7 +20,6 @@ interface IsfrxETH { function maxRedeem(address owner) external view returns (uint256); function maxWithdraw(address owner) external view returns (uint256); function mint(uint256 shares, address receiver) external returns (uint256 assets); - function mintWithSignature(uint256 shares, address receiver, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s) external returns (uint256 assets); function name() external view returns (string memory); function nonces(address) external view returns (uint256); function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; diff --git a/src/frxETHMinter.sol b/src/frxETHMinter.sol index 4565883..75f00c6 100644 --- a/src/frxETHMinter.sol +++ b/src/frxETHMinter.sol @@ -117,7 +117,8 @@ contract frxETHMinter is OperatorRegistry, ReentrancyGuard { /// @notice Deposit batches of ETH to the ETH 2.0 deposit contract /// @dev Usually a bot will call this periodically - function depositEther() external nonReentrant { + /// @param max_deposits Used to prevent gassing out if a whale drops in a huge amount of ETH. Break it down into batches. + function depositEther(uint256 max_deposits) external nonReentrant { // Initial pause check require(!depositEtherPaused, "Depositing ETH is paused"); @@ -125,8 +126,10 @@ contract frxETHMinter is OperatorRegistry, ReentrancyGuard { uint256 numDeposits = (address(this).balance - currentWithheldETH) / DEPOSIT_SIZE; require(numDeposits > 0, "Not enough ETH in contract"); + uint256 loopsToUse = ((numDeposits > max_deposits) ? max_deposits : numDeposits); + // Give each deposit chunk to an empty validator - for (uint256 i = 0; i < numDeposits; ++i) { + for (uint256 i = 0; i < loopsToUse; ++i) { // Get validator information ( bytes memory pubKey, diff --git a/test/frxETHMinter.t.sol b/test/frxETHMinter.t.sol index f4d6265..7f2be3e 100644 --- a/test/frxETHMinter.t.sol +++ b/test/frxETHMinter.t.sol @@ -224,7 +224,7 @@ contract frxETHMinterTest is Test { // Try having the validator deposit. // Should fail due to lack of ETH vm.expectRevert("Not enough ETH in contract"); - minter.depositEther(); + minter.depositEther(10); // Deposit last 1 ETH for frxETH, making the total 32. // Uses submitAndGive as an alternate method. Timelock will get the frxETH but the validator doesn't care @@ -235,12 +235,12 @@ contract frxETHMinterTest is Test { minter.submitAndGive{ value: 1 ether }(FRAX_TIMELOCK); // Move the 32 ETH to the validator - minter.depositEther(); + minter.depositEther(10); // Try having the validator deposit another 32 ETH. // Should fail due to lack of ETH vm.expectRevert("Not enough ETH in contract"); - minter.depositEther(); + minter.depositEther(10); // Deposit 32 ETH for frxETH minter.submit{ value: 32 ether }(); @@ -248,7 +248,7 @@ contract frxETHMinterTest is Test { // Try having the validator deposit another 32 ETH. // Should fail due to lack of a free validator vm.expectRevert("Validator stack is empty"); - minter.depositEther(); + minter.depositEther(10); // Pause submits minter.togglePauseSubmits(); @@ -265,7 +265,7 @@ contract frxETHMinterTest is Test { // Try submitting while paused (should fail) vm.expectRevert("Depositing ETH is paused"); - minter.depositEther(); + minter.depositEther(10); // Unpause validator ETH deposits minter.togglePauseDepositEther(); @@ -274,7 +274,7 @@ contract frxETHMinterTest is Test { minter.addValidator(OperatorRegistry.Validator(pubKeys[1], sigs[1], ddRoots[1])); // Should finally work again - minter.depositEther(); + minter.depositEther(10); vm.stopPrank(); } @@ -304,7 +304,7 @@ contract frxETHMinterTest is Test { // Try having the validator deposit. // Should fail due to lack of ETH because half of it was withheld vm.expectRevert("Not enough ETH in contract"); - minter.depositEther(); + minter.depositEther(10); // Deposit another 32 ETH for frxETH. // 16 ETH will be withheld and the other 16 ETH will be available for the validator @@ -315,7 +315,7 @@ contract frxETHMinterTest is Test { minter.submit{ value: 32 ether }(); // Move the 32 ETH to the validator. Should work now because 16 + 16 = 32 - minter.depositEther(); + minter.depositEther(10); // Set the withhold ratio back to 0 minter.setWithholdRatio(0); @@ -331,7 +331,7 @@ contract frxETHMinterTest is Test { minter.addValidator(OperatorRegistry.Validator(pubKeys[1], sigs[1], ddRoots[1])); // Move the 32 ETH to the validator. Should work immediately - minter.depositEther(); + minter.depositEther(10); vm.stopPrank(); } diff --git a/test/frxETH_sfrxETH_combo.t.sol b/test/frxETH_sfrxETH_combo.t.sol index 5fd1612..9891e7f 100644 --- a/test/frxETH_sfrxETH_combo.t.sol +++ b/test/frxETH_sfrxETH_combo.t.sol @@ -696,118 +696,6 @@ contract xERC4626Test is Test { assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); } - function test_DepositWithSignatureMaxPermit(uint256 fuzz_amount) public { - uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under - getQuickfrxETH(); - - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: owner, - spender: address(sfrxETHtoken), - value: type(uint256).max, - nonce: frxETHtoken.nonces(owner), - deadline: 1 days - }); - - bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - - vm.prank(owner); - if (transfer_amount == 0) vm.expectRevert("ZERO_SHARES"); - sfrxETHtoken.depositWithSignature( - transfer_amount, - permit.owner, - permit.deadline, - true, - v, - r, - s - ); - if (transfer_amount == 0) return; - - assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); - assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); - - // Max allowances never decrease due to spending, per ERC20 - assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), type(uint256).max); - - assertEq(frxETHtoken.nonces(owner), 1); - assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); - } - - function test_MintWithSignatureLimitedPermit(uint256 fuzz_amount) public { - uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under - getQuickfrxETH(); - - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: owner, - spender: address(sfrxETHtoken), - value: transfer_amount, - nonce: frxETHtoken.nonces(owner), - deadline: 1 days - }); - - bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - - vm.prank(owner); - // if (transfer_amount == 0) vm.expectRevert("ZERO_SHARES"); - sfrxETHtoken.mintWithSignature( - transfer_amount, - permit.owner, - permit.deadline, - false, - v, - r, - s - ); - // if (transfer_amount == 0) return; - - assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); - assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); - - assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), 0); - assertEq(frxETHtoken.nonces(owner), 1); - - assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); - } - - function test_MintWithSignatureMaxPermit(uint256 fuzz_amount) public { - uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under - getQuickfrxETH(); - - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: owner, - spender: address(sfrxETHtoken), - value: type(uint256).max, - nonce: frxETHtoken.nonces(owner), - deadline: 1 days - }); - - bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - - vm.prank(owner); - sfrxETHtoken.mintWithSignature( - transfer_amount, - permit.owner, - permit.deadline, - true, - v, - r, - s - ); - - assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); - assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); - - assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), type(uint256).max); - assertEq(frxETHtoken.nonces(owner), 1); - - assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); - } // frxETHMinter submitAndDeposit tests // NOTE: Need to test with a mainnet fork for this not revert From 4c3315e04cc4967e9edca44d9e81087c40ff7a65 Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Thu, 29 Sep 2022 18:18:02 -0700 Subject: [PATCH 2/7] test not working 2 --- test/frxETH_sfrxETH_combo.t.sol | 200 +++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 42 deletions(-) diff --git a/test/frxETH_sfrxETH_combo.t.sol b/test/frxETH_sfrxETH_combo.t.sol index 9891e7f..890765c 100644 --- a/test/frxETH_sfrxETH_combo.t.sol +++ b/test/frxETH_sfrxETH_combo.t.sol @@ -696,6 +696,118 @@ contract xERC4626Test is Test { assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); } + // function test_DepositWithSignatureMaxPermit(uint256 fuzz_amount) public { + // uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under + // getQuickfrxETH(); + + // SigUtils.Permit memory permit = SigUtils.Permit({ + // owner: owner, + // spender: address(sfrxETHtoken), + // value: type(uint256).max, + // nonce: frxETHtoken.nonces(owner), + // deadline: 1 days + // }); + + // bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); + + // (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + // vm.prank(owner); + // if (transfer_amount == 0) vm.expectRevert("ZERO_SHARES"); + // sfrxETHtoken.depositWithSignature( + // transfer_amount, + // permit.owner, + // permit.deadline, + // true, + // v, + // r, + // s + // ); + // if (transfer_amount == 0) return; + + // assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); + // assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); + + // // Max allowances never decrease due to spending, per ERC20 + // assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), type(uint256).max); + + // assertEq(frxETHtoken.nonces(owner), 1); + // assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); + // } + + // function test_MintWithSignatureLimitedPermit(uint256 fuzz_amount) public { + // uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under + // getQuickfrxETH(); + + // SigUtils.Permit memory permit = SigUtils.Permit({ + // owner: owner, + // spender: address(sfrxETHtoken), + // value: transfer_amount, + // nonce: frxETHtoken.nonces(owner), + // deadline: 1 days + // }); + + // bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); + + // (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + // vm.prank(owner); + // // if (transfer_amount == 0) vm.expectRevert("ZERO_SHARES"); + // sfrxETHtoken.mintWithSignature( + // transfer_amount, + // permit.owner, + // permit.deadline, + // false, + // v, + // r, + // s + // ); + // // if (transfer_amount == 0) return; + + // assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); + // assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); + + // assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), 0); + // assertEq(frxETHtoken.nonces(owner), 1); + + // assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); + // } + + // function test_MintWithSignatureMaxPermit(uint256 fuzz_amount) public { + // uint256 transfer_amount = fuzz_amount % (1 ether); // Restrict the fuzz amount to 1 ether and under + // getQuickfrxETH(); + + // SigUtils.Permit memory permit = SigUtils.Permit({ + // owner: owner, + // spender: address(sfrxETHtoken), + // value: type(uint256).max, + // nonce: frxETHtoken.nonces(owner), + // deadline: 1 days + // }); + + // bytes32 digest = sigUtils_frxETH.getTypedDataHash(permit); + + // (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + // vm.prank(owner); + // sfrxETHtoken.mintWithSignature( + // transfer_amount, + // permit.owner, + // permit.deadline, + // true, + // v, + // r, + // s + // ); + + // assertEq(frxETHtoken.balanceOf(owner), 1 ether - transfer_amount); + // assertEq(frxETHtoken.balanceOf(address(sfrxETHtoken)), transfer_amount); + + // assertEq(frxETHtoken.allowance(owner, address(sfrxETHtoken)), type(uint256).max); + // assertEq(frxETHtoken.nonces(owner), 1); + + // assertEq(sfrxETHtoken.balanceOf(owner), transfer_amount); + // } // frxETHMinter submitAndDeposit tests // NOTE: Need to test with a mainnet fork for this not revert @@ -722,7 +834,7 @@ contract xERC4626Test is Test { // OTHER TESTS // =========================================================== - function mintTo(address to, uint256 amount) public { + function mintFXETHTo(address to, uint256 amount) public { vm.prank(frxETHMinterEOA); frxETHtoken.minter_mint(to, amount); } @@ -737,7 +849,7 @@ contract xERC4626Test is Test { } // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), combined); + mintFXETHTo(address(this), combined); frxETHtoken.approve(address(sfrxETHtoken), combined); // Generate some sfrxETH to this testing contract using frxETH @@ -745,7 +857,7 @@ contract xERC4626Test is Test { require(sfrxETHtoken.totalAssets() == seed, "seed"); // Mint frxETH "rewards" to sfrxETH. This mocks earning ETH 2.0 staking rewards. - mintTo(address(sfrxETHtoken), reward); + mintFXETHTo(address(sfrxETHtoken), reward); require(sfrxETHtoken.lastRewardAmount() == 0, "reward"); require(sfrxETHtoken.totalAssets() == seed, "totalassets"); require(sfrxETHtoken.convertToAssets(seed) == seed); // 1:1 still @@ -789,7 +901,7 @@ contract xERC4626Test is Test { } // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), combined); + mintFXETHTo(address(this), combined); frxETHtoken.approve(address(sfrxETHtoken), combined); // Generate some sfrxETH to this testing contract using frxETH @@ -797,7 +909,7 @@ contract xERC4626Test is Test { require(sfrxETHtoken.totalAssets() == seed, "seed"); // Mint frxETH "rewards" to sfrxETH. This mocks earning ETH 2.0 staking rewards. - mintTo(address(sfrxETHtoken), reward); + mintFXETHTo(address(sfrxETHtoken), reward); require(sfrxETHtoken.lastRewardAmount() == 0, "reward"); require(sfrxETHtoken.totalAssets() == seed, "totalassets"); require(sfrxETHtoken.convertToAssets(seed) == seed); // 1:1 still @@ -839,7 +951,7 @@ contract xERC4626Test is Test { // Mint frxETH to this testing contract from nothing, for testing uint256 combined = uint256(deposit1) + uint256(deposit2); - mintTo(address(this), combined); + mintFXETHTo(address(this), combined); // Generate sfrxETH to this testing contract, part 1 frxETHtoken.approve(address(sfrxETHtoken), combined); @@ -857,7 +969,7 @@ contract xERC4626Test is Test { vm.assume(deposit != 0 && withdraw != 0 && withdraw <= deposit); // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), deposit); + mintFXETHTo(address(this), deposit); // Generate some sfrxETH to this testing contract using frxETH frxETHtoken.approve(address(sfrxETHtoken), deposit); @@ -877,17 +989,17 @@ contract xERC4626Test is Test { } // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), seed); + mintFXETHTo(address(this), seed); frxETHtoken.approve(address(sfrxETHtoken), seed); // Generate sfrxETH to the contract sfrxETHtoken.deposit(seed, address(this)); // Mint frxETH "rewards" to sfrxETH. This mocks earning ETH 2.0 staking rewards. - mintTo(address(sfrxETHtoken), reward); + mintFXETHTo(address(sfrxETHtoken), reward); // Sync the rewards - sfrxETHtoken.syncRewards(); + // sfrxETHtoken.syncRewards(); warp = bound(warp, 0, 999); vm.warp(warp); @@ -904,7 +1016,7 @@ contract xERC4626Test is Test { } // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), seed); + mintFXETHTo(address(this), seed); frxETHtoken.approve(address(sfrxETHtoken), seed); // Generate sfrxETH to the contract @@ -913,31 +1025,31 @@ contract xERC4626Test is Test { vm.warp(100); // Sync with no new rewards - sfrxETHtoken.syncRewards(); - require(sfrxETHtoken.lastRewardAmount() == 0); - require(sfrxETHtoken.lastSync() == 100); - require(sfrxETHtoken.rewardsCycleEnd() == 1000); - require(sfrxETHtoken.totalAssets() == seed); - require(sfrxETHtoken.convertToShares(seed) == seed); + // sfrxETHtoken.syncRewards(); + assertEq(sfrxETHtoken.lastRewardAmount(), 0); + assertEq(sfrxETHtoken.lastSync(), 0); + assertEq(sfrxETHtoken.rewardsCycleEnd(), 1000); + assertEq(sfrxETHtoken.totalAssets(), seed); + assertEq(sfrxETHtoken.convertToShares(seed), seed); // Fast forward to next cycle and add rewards vm.warp(1000); // Mint frxETH "rewards" to sfrxETH. This mocks earning ETH 2.0 staking rewards. - mintTo(address(sfrxETHtoken), reward); + mintFXETHTo(address(sfrxETHtoken), reward); // Sync with rewards this time sfrxETHtoken.syncRewards(); - require(sfrxETHtoken.lastRewardAmount() == reward); - require(sfrxETHtoken.totalAssets() == seed); - require(sfrxETHtoken.convertToShares(seed) == seed); + assertEq(sfrxETHtoken.lastRewardAmount(), reward); + assertEq(sfrxETHtoken.totalAssets(), seed); + assertEq(sfrxETHtoken.convertToShares(seed), seed); // Fast forward vm.warp(2000); - require(sfrxETHtoken.lastRewardAmount() == reward); - require(sfrxETHtoken.totalAssets() == combined); - require(sfrxETHtoken.convertToAssets(seed) == combined); + assertEq(sfrxETHtoken.lastRewardAmount(), reward); + assertEq(sfrxETHtoken.totalAssets(), combined); + assertEq(sfrxETHtoken.convertToAssets(seed), combined); assertEq(sfrxETHtoken.convertToShares(combined), seed); } @@ -950,7 +1062,7 @@ contract xERC4626Test is Test { } // Mint frxETH to this testing contract from nothing, for testing - mintTo(address(this), seed); + mintFXETHTo(address(this), seed); frxETHtoken.approve(address(sfrxETHtoken), seed); // Generate sfrxETH to the contract @@ -959,31 +1071,35 @@ contract xERC4626Test is Test { vm.warp(100); // Mint frxETH "rewards" to sfrxETH. This mocks earning ETH 2.0 staking rewards. - mintTo(address(sfrxETHtoken), reward); + mintFXETHTo(address(sfrxETHtoken), reward); // Sync with new rewards - sfrxETHtoken.syncRewards(); - require(sfrxETHtoken.lastRewardAmount() == reward); - require(sfrxETHtoken.lastSync() == 100); - require(sfrxETHtoken.rewardsCycleEnd() == 1000); - require(sfrxETHtoken.totalAssets() == seed); - require(sfrxETHtoken.convertToShares(seed) == seed); // 1:1 still + assertEq(sfrxETHtoken.lastSync(), 0, 'sfrxETHtoken.lastSync'); + assertEq(sfrxETHtoken.rewardsCycleEnd(), 1000, 'sfrxETHtoken.rewardsCycleEnd'); + assertEq(sfrxETHtoken.totalAssets(), seed, 'sfrxETHtoken.totalAssets'); + assertEq(sfrxETHtoken.convertToShares(seed), seed, 'sfrxETHtoken.convertToShares'); // 1:1 still - // Fast forward to next cycle and add rewards + // Fast forward to next cycle and check rewards vm.warp(1000); - mintTo(address(sfrxETHtoken), reward2); // seed new rewards + sfrxETHtoken.syncRewards(); + assertEq(sfrxETHtoken.lastRewardAmount(), reward, 'sfrxETHtoken.lastRewardAmount [1st]'); + + // Add a second set of rewards to this cycle + mintFXETHTo(address(sfrxETHtoken), reward2); // seed new rewards + + // Fast forward 10 cycles + vm.warp(10000); // Sync the rewards sfrxETHtoken.syncRewards(); - require(sfrxETHtoken.lastRewardAmount() == reward2); - require(sfrxETHtoken.totalAssets() == combined1); - require(sfrxETHtoken.convertToAssets(seed) == combined1); + assertEq(sfrxETHtoken.lastRewardAmount(), reward2, 'sfrxETHtoken.lastRewardAmount [2nd]'); + assertEq(sfrxETHtoken.totalAssets(), combined1, 'sfrxETHtoken.totalAssets [2nd]'); + assertEq(sfrxETHtoken.convertToAssets(seed), combined1, 'sfrxETHtoken.convertToAssets [2nd]'); - // Fast forward two cycles + // Fast forward two cycles to make sure nothing changed vm.warp(2000); - - require(sfrxETHtoken.lastRewardAmount() == reward2); - require(sfrxETHtoken.totalAssets() == combined2); - require(sfrxETHtoken.convertToAssets(seed) == combined2); + assertEq(sfrxETHtoken.lastRewardAmount(), reward2, 'sfrxETHtoken.lastRewardAmount [3rd]'); + assertEq(sfrxETHtoken.totalAssets(), combined2, 'sfrxETHtoken.totalAssets [3rd]'); + assertEq(sfrxETHtoken.convertToAssets(seed), combined2, 'sfrxETHtoken.convertToAssets [3rd]'); } } \ No newline at end of file From 8f1035caaa7b4e5469dccaf5acaa9c884d0b7082 Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Sat, 1 Oct 2022 10:21:33 -0700 Subject: [PATCH 3/7] frxETHMinter loop fix --- lib/ERC4626 | 2 +- src/frxETHMinter.sol | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ERC4626 b/lib/ERC4626 index 6cf2bee..643cd04 160000 --- a/lib/ERC4626 +++ b/lib/ERC4626 @@ -1 +1 @@ -Subproject commit 6cf2bee5d784169acb02cc6ac0489ca197a4f149 +Subproject commit 643cd044fac34bcbf64e1c3790a5126fec0dbec1 diff --git a/src/frxETHMinter.sol b/src/frxETHMinter.sol index 75f00c6..a8e037e 100644 --- a/src/frxETHMinter.sol +++ b/src/frxETHMinter.sol @@ -126,7 +126,9 @@ contract frxETHMinter is OperatorRegistry, ReentrancyGuard { uint256 numDeposits = (address(this).balance - currentWithheldETH) / DEPOSIT_SIZE; require(numDeposits > 0, "Not enough ETH in contract"); - uint256 loopsToUse = ((numDeposits > max_deposits) ? max_deposits : numDeposits); + uint256 loopsToUse = numDeposits; + if (max_deposits == 0) loopsToUse = numDeposits; + else if (numDeposits > max_deposits) loopsToUse = max_deposits; // Give each deposit chunk to an empty validator for (uint256 i = 0; i < loopsToUse; ++i) { From 7033e3b44c4128ce47ca3f8425b8aace60f4b109 Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Sat, 1 Oct 2022 14:31:39 -0700 Subject: [PATCH 4/7] submodule update --- README.md | 7 +++++-- SAMPLE.env | 1 + lib/openzeppelin-contracts | 2 +- script/deployMainnet.s.sol | 30 ++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 script/deployMainnet.s.sol diff --git a/README.md b/README.md index 3549245..843807b 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,13 @@ or ```source .env && forge test --fork-url $MAINNET_RPC_URL -m test_frxETHMinter ### Goerli #### Single deploy -```forge create src/frxETH.sol:frxETH --private-key $PRIVATE_KEY --rpc-url $GOERLI_RPC_URL --verify --optimize --etherscan-api-key $ETHERSCAN_KEY --constructor-args $FRXETH_OWNER $TIMELOCK_ADDRESS``` +```source .env && forge create src/frxETH.sol:frxETH --private-key $PRIVATE_KEY --rpc-url $GOERLI_RPC_URL --verify --optimize --etherscan-api-key $ETHERSCAN_KEY --constructor-args $FRXETH_OWNER $TIMELOCK_ADDRESS``` #### Group deploy script -```forge script script/deployGoerli.s.sol:Deploy --rpc-url $GOERLI_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY``` +Goerli +```source .env && forge script script/deployGoerli.s.sol:Deploy --rpc-url $GOERLI_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY``` +Mainnet +```source .env && forge script script/deployMainnet.s.sol:Deploy --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY``` #### Etherscan Verification Sometimes the deploy scripts above fail with Etherscan's verification API. In that case, use: diff --git a/SAMPLE.env b/SAMPLE.env index 4209511..c7d5b16 100644 --- a/SAMPLE.env +++ b/SAMPLE.env @@ -33,6 +33,7 @@ VALIDATOR_TEST_DDROOT5="0x4645ce50940a306f2a120a8f2a80fd86bd9567760c3a742cf50aee # Live # ================================= +VALIDATOR_MAINNET_WITHDRAWAL_CREDENTIALS="" VALIDATOR_GOERLI_PUBKEY1="" VALIDATOR_GOERLI_SIG1="" VALIDATOR_GOERLI_DDROOT1="" diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 26dddee..561d106 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 26dddee1c05ff91264ae24c5c172bd05827cf5d8 +Subproject commit 561d1061fc568f04c7a65853538e834a889751e8 diff --git a/script/deployMainnet.s.sol b/script/deployMainnet.s.sol new file mode 100644 index 0000000..0602509 --- /dev/null +++ b/script/deployMainnet.s.sol @@ -0,0 +1,30 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; // Gives vm and console +import {frxETH} from "../src/frxETH.sol"; +import {sfrxETH, ERC20} from "../src/sfrxETH.sol"; +import {frxETHMinter, OperatorRegistry} from "../src/frxETHMinter.sol"; + +contract Deploy is Script { + address constant OWNER_ADDRESS = 0xB1748C79709f4Ba2Dd82834B8c82D4a505003f27; + address constant TIMELOCK_ADDRESS = 0x8412ebf45bAC1B340BbE8F318b928C466c4E39CA; + + address constant DEPOSIT_CONTRACT_ADDRESS = 0xB1748C79709f4Ba2Dd82834B8c82D4a505003f27; + bytes WITHDRAWAL_CREDENTIALS; + uint32 constant REWARDS_CYCLE_LENGTH = 1000; + + function run() public { + vm.startBroadcast(); + WITHDRAWAL_CREDENTIALS = vm.envBytes('VALIDATOR_MAINNET_WITHDRAWAL_CREDENTIALS'); + + frxETH fe = new frxETH(OWNER_ADDRESS, TIMELOCK_ADDRESS); + sfrxETH sfe = new sfrxETH(ERC20(address(fe)), REWARDS_CYCLE_LENGTH); + frxETHMinter fem = new frxETHMinter(DEPOSIT_CONTRACT_ADDRESS, address(fe), address(sfe), OWNER_ADDRESS, TIMELOCK_ADDRESS, WITHDRAWAL_CREDENTIALS); + + // // Post deploy + // fe.addMinter(address(fem)); + + vm.stopBroadcast(); + } +} From 6704d071d8074fc3c779efab9074bdf5fd655bba Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Sat, 1 Oct 2022 14:38:27 -0700 Subject: [PATCH 5/7] mini changes --- .gitmodules | 8 ++++---- lib/ERC4626 | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitmodules b/.gitmodules index 1f42d1c..3cdf174 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,16 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std - # ignore = dirty + ignore = dirty [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts/ - # ignore = dirty + ignore = dirty [submodule "lib/solmate"] path = lib/solmate url = https://github.com/Rari-Capital/solmate - # ignore = dirty + ignore = dirty [submodule "lib/ERC4626"] path = lib/ERC4626 url = https://github.com/corddry/ERC4626 - # ignore = dirty \ No newline at end of file + ignore = dirty \ No newline at end of file diff --git a/lib/ERC4626 b/lib/ERC4626 index 643cd04..6ebfacc 160000 --- a/lib/ERC4626 +++ b/lib/ERC4626 @@ -1 +1 @@ -Subproject commit 643cd044fac34bcbf64e1c3790a5126fec0dbec1 +Subproject commit 6ebfacc4f9ccfd76d4fa90a8989a4b5eb0977923 From 16e58d22d112af520d587c12cb5f7a695471762b Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Sat, 1 Oct 2022 14:41:10 -0700 Subject: [PATCH 6/7] mini changes 2 --- lib/ERC4626 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ERC4626 b/lib/ERC4626 index 6ebfacc..643cd04 160000 --- a/lib/ERC4626 +++ b/lib/ERC4626 @@ -1 +1 @@ -Subproject commit 6ebfacc4f9ccfd76d4fa90a8989a4b5eb0977923 +Subproject commit 643cd044fac34bcbf64e1c3790a5126fec0dbec1 From 40421b7c097dc4585a0c8a1fae16c9ce53c0ae6a Mon Sep 17 00:00:00 2001 From: Travis Moore Date: Mon, 3 Oct 2022 16:14:30 -0700 Subject: [PATCH 7/7] submodule stuff --- .gitmodules | 8 +- README.md | 1 + flattened.sol | 1135 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/ERC4626 | 2 +- 4 files changed, 1141 insertions(+), 5 deletions(-) create mode 100644 flattened.sol diff --git a/.gitmodules b/.gitmodules index 3cdf174..1f42d1c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,16 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std - ignore = dirty + # ignore = dirty [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts/ - ignore = dirty + # ignore = dirty [submodule "lib/solmate"] path = lib/solmate url = https://github.com/Rari-Capital/solmate - ignore = dirty + # ignore = dirty [submodule "lib/ERC4626"] path = lib/ERC4626 url = https://github.com/corddry/ERC4626 - ignore = dirty \ No newline at end of file + # ignore = dirty \ No newline at end of file diff --git a/README.md b/README.md index 843807b..85cfd3e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ 2) Install [foundry](https://book.getfoundry.sh/getting-started/installation) 3) ```forge install``` 4) ```git submodule update --init --recursive``` +4a) ```cd ./lib/ERC4626 && git checkout main```. This should switch it to ```corddry```'s fork. 5) (Optional) Occasionally update / pull your submodules to keep them up to date. ```git submodule update --recursive --remote``` 6) Create your own .env and copy SAMPLE.env into there. Sample mainnet validator deposit keys are in test/deposit_data-TESTS-MAINNET.json if you need more. 7) You don't need to add PRIVATE_KEY, ETHERSCAN_KEY, or FRXETH_OWNER if you are not actually deploying on live mainnet diff --git a/flattened.sol b/flattened.sol new file mode 100644 index 0000000..332f955 --- /dev/null +++ b/flattened.sol @@ -0,0 +1,1135 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ============================== sfrxETH ============================= +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance + +// Primary Author(s) +// Jack Corddry: https://github.com/corddry +// Nader Ghazvini: https://github.com/amirnader-ghazvini + +// Reviewer(s) / Contributor(s) +// Sam Kazemian: https://github.com/samkazemian +// Dennett: https://github.com/denett +// Travis Moore: https://github.com/FortisFortuna +// Jamie Turley: https://github.com/jyturley + +// Rewards logic inspired by xERC20 (https://github.com/ZeframLou/playpen/blob/main/src/xERC20.sol) + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20 { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public immutable decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal immutable INITIAL_CHAIN_ID; + + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) { + name = _name; + symbol = _symbol; + decimals = _decimals; + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + allowance[recoveredAddress][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } +} + +/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol) +/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. +/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. +library SafeTransferLib { + /*////////////////////////////////////////////////////////////// + ETH OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferETH(address to, uint256 amount) internal { + bool success; + + assembly { + // Transfer the ETH and store if it succeeded or not. + success := call(gas(), to, amount, 0, 0, 0, 0) + } + + require(success, "ETH_TRANSFER_FAILED"); + } + + /*////////////////////////////////////////////////////////////// + ERC20 OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferFrom( + ERC20 token, + address from, + address to, + uint256 amount + ) internal { + bool success; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), from) // Append the "from" argument. + mstore(add(freeMemoryPointer, 36), to) // Append the "to" argument. + mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + // Counterintuitively, this call must be positioned second to the or() call in the + // surrounding and() call or else returndatasize() will be zero during the computation. + call(gas(), token, 0, freeMemoryPointer, 100, 0, 32) + ) + } + + require(success, "TRANSFER_FROM_FAILED"); + } + + function safeTransfer( + ERC20 token, + address to, + uint256 amount + ) internal { + bool success; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + // Counterintuitively, this call must be positioned second to the or() call in the + // surrounding and() call or else returndatasize() will be zero during the computation. + call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) + ) + } + + require(success, "TRANSFER_FAILED"); + } + + function safeApprove( + ERC20 token, + address to, + uint256 amount + ) internal { + bool success; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + // Counterintuitively, this call must be positioned second to the or() call in the + // surrounding and() call or else returndatasize() will be zero during the computation. + call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) + ) + } + + require(success, "APPROVE_FAILED"); + } +} + +/// @notice Arithmetic library with operations for fixed-point numbers. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) +/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) +library FixedPointMathLib { + /*////////////////////////////////////////////////////////////// + SIMPLIFIED FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. + + function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. + } + + function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. + } + + function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. + } + + function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. + } + + /*////////////////////////////////////////////////////////////// + LOW LEVEL FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function mulDivDown( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + assembly { + // Store x * y in z for now. + z := mul(x, y) + + // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) + if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { + revert(0, 0) + } + + // Divide z by the denominator. + z := div(z, denominator) + } + } + + function mulDivUp( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + assembly { + // Store x * y in z for now. + z := mul(x, y) + + // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) + if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { + revert(0, 0) + } + + // First, divide z - 1 by the denominator and add 1. + // We allow z - 1 to underflow if z is 0, because we multiply the + // end result by 0 if z is zero, ensuring we return 0 if z is zero. + z := mul(iszero(iszero(z)), add(div(sub(z, 1), denominator), 1)) + } + } + + function rpow( + uint256 x, + uint256 n, + uint256 scalar + ) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + // 0 ** 0 = 1 + z := scalar + } + default { + // 0 ** n = 0 + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + // If n is even, store scalar in z for now. + z := scalar + } + default { + // If n is odd, store x in z for now. + z := x + } + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, scalar) + + for { + // Shift n right by 1 before looping to halve it. + n := shr(1, n) + } n { + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) + } { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { + revert(0, 0) + } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. + let xxRound := add(xx, half) + + // Revert if xx + half overflowed. + if lt(xxRound, xx) { + revert(0, 0) + } + + // Set x to scaled xxRound. + x := div(xxRound, scalar) + + // If n is even: + if mod(n, 2) { + // Compute z * x. + let zx := mul(z, x) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } + } + + // Round to the nearest number. + let zxRound := add(zx, half) + + // Revert if zx + half overflowed. + if lt(zxRound, zx) { + revert(0, 0) + } + + // Return properly scaled zxRound. + z := div(zxRound, scalar) + } + } + } + } + } + + /*////////////////////////////////////////////////////////////// + GENERAL NUMBER UTILITIES + //////////////////////////////////////////////////////////////*/ + + function sqrt(uint256 x) internal pure returns (uint256 z) { + assembly { + let y := x // We start y at x, which will help us make our initial estimate. + + z := 181 // The "correct" value is 1, but this saves a multiplication later. + + // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad + // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. + + // We check y >= 2^(k + 8) but shift right by k bits + // each branch to ensure that if x >= 256, then y >= 256. + if iszero(lt(y, 0x10000000000000000000000000000000000)) { + y := shr(128, y) + z := shl(64, z) + } + if iszero(lt(y, 0x1000000000000000000)) { + y := shr(64, y) + z := shl(32, z) + } + if iszero(lt(y, 0x10000000000)) { + y := shr(32, y) + z := shl(16, z) + } + if iszero(lt(y, 0x1000000)) { + y := shr(16, y) + z := shl(8, z) + } + + // Goal was to get z*z*y within a small factor of x. More iterations could + // get y in a tighter range. Currently, we will have y in [256, 256*2^16). + // We ensured y >= 256 so that the relative difference between y and y+1 is small. + // That's not possible if x < 256 but we can just verify those cases exhaustively. + + // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256. + // Correctness can be checked exhaustively for x < 256, so we assume y >= 256. + // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps. + + // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range + // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256. + + // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate + // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18. + + // There is no overflow risk here since y < 2^136 after the first branch above. + z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181. + + // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // If x+1 is a perfect square, the Babylonian method cycles between + // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor. + // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division + // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. + // If you don't care whether the floor or ceil square root is returned, you can remove this statement. + z := sub(z, lt(div(x, z), z)) + } + } + + function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + // Mod x by y. Note this will return + // 0 instead of reverting if y is zero. + z := mod(x, y) + } + } + + function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { + assembly { + // Divide x by y. Note this will return + // 0 instead of reverting if y is zero. + r := div(x, y) + } + } + + function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + // Add 1 to x * y if x % y > 0. Note this will + // return 0 instead of reverting if y is zero. + z := add(gt(mod(x, y), 0), div(x, y)) + } + } +} + +/// @notice Minimal ERC4626 tokenized Vault implementation. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/mixins/ERC4626.sol) +abstract contract ERC4626 is ERC20 { + using SafeTransferLib for ERC20; + using FixedPointMathLib for uint256; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /*////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + ERC20 public immutable asset; + + constructor( + ERC20 _asset, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol, _asset.decimals()) { + asset = _asset; + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ + + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // Check for rounding error since we round down in previewDeposit. + require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. + + // Need to transfer before minting or ERC777s could reenter. + asset.safeTransferFrom(msg.sender, address(this), assets); + + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + + afterDeposit(assets, shares); + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + // Check for rounding error since we round down in previewRedeem. + require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS"); + + beforeWithdraw(assets, shares); + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + asset.safeTransfer(receiver, assets); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function totalAssets() public view virtual returns (uint256); + + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); + } + + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply); + } + + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + function previewMint(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivUp(totalAssets(), supply); + } + + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets()); + } + + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf[owner]); + } + + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf[owner]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} + + function afterDeposit(uint256 assets, uint256 shares) internal virtual {} +} + +/// @notice Safe unsigned integer casting library that reverts on overflow. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeCastLib.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) +library SafeCastLib { + function safeCastTo248(uint256 x) internal pure returns (uint248 y) { + require(x < 1 << 248); + + y = uint248(x); + } + + function safeCastTo224(uint256 x) internal pure returns (uint224 y) { + require(x < 1 << 224); + + y = uint224(x); + } + + function safeCastTo192(uint256 x) internal pure returns (uint192 y) { + require(x < 1 << 192); + + y = uint192(x); + } + + function safeCastTo160(uint256 x) internal pure returns (uint160 y) { + require(x < 1 << 160); + + y = uint160(x); + } + + function safeCastTo128(uint256 x) internal pure returns (uint128 y) { + require(x < 1 << 128); + + y = uint128(x); + } + + function safeCastTo96(uint256 x) internal pure returns (uint96 y) { + require(x < 1 << 96); + + y = uint96(x); + } + + function safeCastTo64(uint256 x) internal pure returns (uint64 y) { + require(x < 1 << 64); + + y = uint64(x); + } + + function safeCastTo32(uint256 x) internal pure returns (uint32 y) { + require(x < 1 << 32); + + y = uint32(x); + } + + function safeCastTo24(uint256 x) internal pure returns (uint24 y) { + require(x < 1 << 24); + + y = uint24(x); + } + + function safeCastTo16(uint256 x) internal pure returns (uint16 y) { + require(x < 1 << 16); + + y = uint16(x); + } + + function safeCastTo8(uint256 x) internal pure returns (uint8 y) { + require(x < 1 << 8); + + y = uint8(x); + } +} + +// Rewards logic inspired by xERC20 (https://github.com/ZeframLou/playpen/blob/main/src/xERC20.sol) + +/** + @title An xERC4626 Single Staking Contract Interface + @notice This contract allows users to autocompound rewards denominated in an underlying reward token. + It is fully compatible with [ERC4626](https://eips.ethereum.org/EIPS/eip-4626) allowing for DeFi composability. + It maintains balances using internal accounting to prevent instantaneous changes in the exchange rate. + NOTE: an exception is at contract creation, when a reward cycle begins before the first deposit. After the first deposit, exchange rate updates smoothly. + + Operates on "cycles" which distribute the rewards surplus over the internal balance to users linearly over the remainder of the cycle window. +*/ +interface IxERC4626 { + /*//////////////////////////////////////////////////////// + Custom Errors + ////////////////////////////////////////////////////////*/ + + /// @dev thrown when syncing before cycle ends. + error SyncError(); + + /*//////////////////////////////////////////////////////// + Events + ////////////////////////////////////////////////////////*/ + + /// @dev emit every time a new rewards cycle starts + event NewRewardsCycle(uint32 indexed cycleEnd, uint256 rewardAmount); + + /*//////////////////////////////////////////////////////// + View Methods + ////////////////////////////////////////////////////////*/ + + /// @notice the maximum length of a rewards cycle + function rewardsCycleLength() external view returns (uint32); + + /// @notice the effective start of the current cycle + /// NOTE: This will likely be after `rewardsCycleEnd - rewardsCycleLength` as this is set as block.timestamp of the last `syncRewards` call. + function lastSync() external view returns (uint32); + + /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. + function rewardsCycleEnd() external view returns (uint32); + + /// @notice the amount of rewards distributed in a the most recent cycle + function lastRewardAmount() external view returns (uint192); + + /*//////////////////////////////////////////////////////// + State Changing Methods + ////////////////////////////////////////////////////////*/ + + /// @notice Distributes rewards to xERC4626 holders. + /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. + function syncRewards() external; +} + +/** + @title An xERC4626 Single Staking Contract + @notice This contract allows users to autocompound rewards denominated in an underlying reward token. + It is fully compatible with [ERC4626](https://eips.ethereum.org/EIPS/eip-4626) allowing for DeFi composability. + It maintains balances using internal accounting to prevent instantaneous changes in the exchange rate. + NOTE: an exception is at contract creation, when a reward cycle begins before the first deposit. After the first deposit, exchange rate updates smoothly. + + Operates on "cycles" which distribute the rewards surplus over the internal balance to users linearly over the remainder of the cycle window. +*/ +abstract contract xERC4626 is IxERC4626, ERC4626 { + using SafeCastLib for *; + + /// @notice the maximum length of a rewards cycle + uint32 public immutable rewardsCycleLength; + + /// @notice the effective start of the current cycle + uint32 public lastSync; + + /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. + uint32 public rewardsCycleEnd; + + /// @notice the amount of rewards distributed in a the most recent cycle. + uint192 public lastRewardAmount; + + uint256 internal storedTotalAssets; + + constructor(uint32 _rewardsCycleLength) { + rewardsCycleLength = _rewardsCycleLength; + // seed initial rewardsCycleEnd + rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; + } + + /// @notice Compute the amount of tokens available to share holders. + /// Increases linearly during a reward distribution period from the sync call, not the cycle start. + function totalAssets() public view override returns (uint256) { + // cache global vars + uint256 storedTotalAssets_ = storedTotalAssets; + uint192 lastRewardAmount_ = lastRewardAmount; + uint32 rewardsCycleEnd_ = rewardsCycleEnd; + uint32 lastSync_ = lastSync; + + if (block.timestamp >= rewardsCycleEnd_) { + // no rewards or rewards fully unlocked + // entire reward amount is available + return storedTotalAssets_ + lastRewardAmount_; + } + + // rewards not fully unlocked + // add unlocked rewards to stored total + uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); + return storedTotalAssets_ + unlockedRewards; + } + + // Update storedTotalAssets on withdraw/redeem + function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { + super.beforeWithdraw(amount, shares); + storedTotalAssets -= amount; + } + + // Update storedTotalAssets on deposit/mint + function afterDeposit(uint256 amount, uint256 shares) internal virtual override { + storedTotalAssets += amount; + super.afterDeposit(amount, shares); + } + + /// @notice Distributes rewards to xERC4626 holders. + /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. + function syncRewards() public virtual { + uint192 lastRewardAmount_ = lastRewardAmount; + uint32 timestamp = block.timestamp.safeCastTo32(); + + if (timestamp < rewardsCycleEnd) revert SyncError(); + + uint256 storedTotalAssets_ = storedTotalAssets; + uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; + + storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE + + uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; + + if (end - timestamp < rewardsCycleLength / 20) { + end += rewardsCycleLength; + } + + // Combined single SSTORE + lastRewardAmount = nextRewards.safeCastTo192(); + lastSync = timestamp; + rewardsCycleEnd = end; + + emit NewRewardsCycle(end, nextRewards); + } +} + +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + // On the first call to nonReentrant, _status will be _NOT_ENTERED + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + } + + function _nonReentrantAfter() private { + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} + +/// @title Vault token for staked frxETH +/// @notice Is a vault that takes frxETH and gives you sfrxETH erc20 tokens +/** @dev Exchange rate between frxETH and sfrxETH floats, you can convert your sfrxETH for more frxETH over time. + Exchange rate increases as the frax msig mints new frxETH corresponding to the staking yield and drops it into the vault (sfrxETH contract). + There is a short time period, “cycles” which the exchange rate increases linearly over. This is to prevent gaming the exchange rate (MEV). + The cycles are constant length, but calling syncRewards slightly into a would-be cycle keeps the same would-be endpoint (so cycle ends are every X seconds). + Someone must call syncRewards, which queues any new frxETH in the contract to be added to the redeemable amount. + sfrxETH adheres to ERC-4626 vault specs + Mint vs Deposit + mint() - deposit targeting a specific number of sfrxETH out + deposit() - deposit knowing a specific number of frxETH in */ +contract sfrxETH is xERC4626, ReentrancyGuard { + + modifier andSync { + if (block.timestamp >= rewardsCycleEnd) { syncRewards(); } + _; + } + + /* ========== CONSTRUCTOR ========== */ + constructor(ERC20 _underlying, uint32 _rewardsCycleLength) + ERC4626(_underlying, "Staked Frax Ether", "sfrxETH") + xERC4626(_rewardsCycleLength) + {} + + /// @notice inlines syncRewards with deposits when able + function deposit(uint256 assets, address receiver) public override andSync returns (uint256 shares) { + return super.deposit(assets, receiver); + } + + /// @notice inlines syncRewards with mints when able + function mint(uint256 shares, address receiver) public override andSync returns (uint256 assets) { + return super.mint(shares, receiver); + } + + /// @notice inlines syncRewards with withdrawals when able + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override andSync returns (uint256 shares) { + return super.withdraw(assets, receiver, owner); + } + + /// @notice inlines syncRewards with redemptions when able + function redeem( + uint256 shares, + address receiver, + address owner + ) public override andSync returns (uint256 assets) { + return super.redeem(shares, receiver, owner); + } + + /// @notice How much frxETH is 1E18 sfrxETH worth. Price is in ETH, not USD + function pricePerShare() public view returns (uint256) { + return convertToAssets(1e18); + } + + /// @notice Approve and deposit() in one transaction + function depositWithSignature( + uint256 assets, + address receiver, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external nonReentrant returns (uint256 shares) { + uint256 amount = approveMax ? type(uint256).max : assets; + asset.permit(msg.sender, address(this), amount, deadline, v, r, s); + return (deposit(assets, receiver)); + } + +} diff --git a/lib/ERC4626 b/lib/ERC4626 index 643cd04..6cf2bee 160000 --- a/lib/ERC4626 +++ b/lib/ERC4626 @@ -1 +1 @@ -Subproject commit 643cd044fac34bcbf64e1c3790a5126fec0dbec1 +Subproject commit 6cf2bee5d784169acb02cc6ac0489ca197a4f149