-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Another deletion for review on updated master
- Loading branch information
1 parent
a7bbdd6
commit 1a5d72e
Showing
2 changed files
with
0 additions
and
338 deletions.
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 |
---|---|---|
@@ -1,134 +0,0 @@ | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
pragma solidity ^0.8.13; | ||
|
||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
contract RewardsCollector is Ownable { | ||
event CollectedReward( | ||
address withdrawalCredential, | ||
uint256 withdrawalFee, | ||
address owner, | ||
uint256 ownerFee | ||
); | ||
|
||
// 1 - fee % will go to the user in this address. | ||
address public withdrawalCredential; | ||
|
||
// Fee's numerator. | ||
uint32 public feeNumerator; | ||
|
||
// Fee denominator, if `feeNumerator = 500`, | ||
// the tax is 500/10000 = 5/100 = 5%. | ||
uint32 public constant FEE_DENOMINATOR = 10000; | ||
|
||
// Allow receiving MEV and other rewards. | ||
receive() external payable {} | ||
|
||
function collectRewards() public payable { | ||
uint256 ownerAmount = (address(this).balance * feeNumerator) / | ||
FEE_DENOMINATOR; | ||
uint256 returnedAmount = address(this).balance - ownerAmount; | ||
require( | ||
ownerAmount != 0 || returnedAmount != 0, | ||
"Nothing to distribute" | ||
); | ||
address owner = owner(); | ||
emit CollectedReward( | ||
withdrawalCredential, | ||
returnedAmount, | ||
owner, | ||
ownerAmount | ||
); | ||
// This can be used to call this contract again (reentrancy) | ||
// but since all funds from this contract are used for the owner | ||
payable(owner).transfer(ownerAmount); | ||
(bool sent, ) = payable(withdrawalCredential).call{ | ||
value: returnedAmount | ||
}(""); | ||
require(sent, "Failed to send Ether back to withdrawal credential"); | ||
} | ||
|
||
constructor(address _withdrawalCredential, uint32 _feeNumerator) { | ||
withdrawalCredential = _withdrawalCredential; | ||
feeNumerator = _feeNumerator; | ||
} | ||
|
||
function changeFee(uint32 _newFeeNumerator) public onlyOwner { | ||
feeNumerator = _newFeeNumerator; | ||
} | ||
} | ||
|
||
contract FeeRewardsManager is Ownable { | ||
uint32 public defaultFeeNumerator; | ||
|
||
constructor(uint32 _defaultFeeNumerator) { | ||
defaultFeeNumerator = _defaultFeeNumerator; | ||
} | ||
|
||
event ContractDeployed(address contractAddress, uint32 feeNumerator); | ||
|
||
function changeDefaultFee(uint32 _newFeeNumerator) public onlyOwner { | ||
defaultFeeNumerator = _newFeeNumerator; | ||
} | ||
|
||
function createFeeContract( | ||
address _withdrawalCredential | ||
) public returns (address payable) { | ||
bytes32 withdrawalCredentialBytes = bytes32( | ||
uint256(uint160(_withdrawalCredential)) | ||
); | ||
address addr = address( | ||
// Uses CREATE2 opcode. | ||
new RewardsCollector{salt: withdrawalCredentialBytes}( | ||
_withdrawalCredential, | ||
defaultFeeNumerator | ||
) | ||
); | ||
emit ContractDeployed(addr, defaultFeeNumerator); | ||
return payable(addr); | ||
} | ||
|
||
function predictFeeContractAddress( | ||
address _withdrawalCredential | ||
) public view returns (address) { | ||
bytes memory bytecode = type(RewardsCollector).creationCode; | ||
bytecode = abi.encodePacked( | ||
bytecode, | ||
abi.encode(_withdrawalCredential, defaultFeeNumerator) | ||
); | ||
bytes32 withdrawalCredentialBytes = bytes32( | ||
uint256(uint160(_withdrawalCredential)) | ||
); | ||
bytes32 hash = keccak256( | ||
abi.encodePacked( | ||
bytes1(0xff), | ||
address(this), | ||
withdrawalCredentialBytes, | ||
keccak256(bytecode) | ||
) | ||
); | ||
return address(uint160(uint(hash))); | ||
} | ||
|
||
function changeFee( | ||
address payable _feeContract, | ||
uint32 _newFee | ||
) public onlyOwner { | ||
RewardsCollector(_feeContract).changeFee(_newFee); | ||
} | ||
|
||
function batchCollectRewards( | ||
address payable[] calldata feeAddresses | ||
) public { | ||
for (uint32 i = 0; i < feeAddresses.length; ++i) { | ||
RewardsCollector(feeAddresses[i]).collectRewards(); | ||
} | ||
} | ||
|
||
receive() external payable {} | ||
|
||
// Withdraws Eth from the manager contract. | ||
function getEth(address addr) external onlyOwner { | ||
payable(addr).transfer(address(this).balance); | ||
} | ||
} | ||
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 |
---|---|---|
@@ -1,204 +0,0 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import "forge-std/Test.sol"; | ||
import "forge-std/console.sol"; | ||
import "../src/FeeRewardsManager.sol"; | ||
|
||
contract ReentrantAttack { | ||
fallback() external payable { | ||
RewardsCollector(payable(msg.sender)).collectRewards(); | ||
} | ||
} | ||
|
||
contract ChangeOwnerContract { | ||
fallback() external payable { | ||
RewardsCollector(payable(msg.sender)).transferOwnership(address(0x200)); | ||
} | ||
} | ||
|
||
contract WithdrawalContract { | ||
fallback() external payable {} | ||
} | ||
|
||
contract FeeRewardsTest is Test { | ||
FeeRewardsManager public feeRewardsManager; | ||
|
||
function setUp() public { | ||
feeRewardsManager = new FeeRewardsManager(2800); | ||
} | ||
|
||
function testHappyPath() public { | ||
address withdrawalCredential = address(100); | ||
vm.deal(address(0), 10 ether); | ||
address derivedAddr = feeRewardsManager.predictFeeContractAddress( | ||
withdrawalCredential | ||
); | ||
|
||
vm.deal(derivedAddr, 10 ether); | ||
|
||
address payable addr = feeRewardsManager.createFeeContract( | ||
withdrawalCredential | ||
); | ||
|
||
//derived address matches the function to get one. | ||
assertEq(derivedAddr, addr); | ||
|
||
// derived address has the parent's as owner | ||
assertEq( | ||
address(feeRewardsManager), | ||
RewardsCollector(payable(addr)).owner() | ||
); | ||
|
||
uint256 amountInContract = address(addr).balance; | ||
|
||
// We've got 10 ether in the contract. | ||
assertEq(amountInContract, 10 ether); | ||
|
||
RewardsCollector(payable(addr)).collectRewards(); | ||
|
||
// User receives 72%. | ||
assertEq(withdrawalCredential.balance, 7.2 ether); | ||
|
||
// We receive 28%. | ||
assertEq(address(feeRewardsManager).balance, 2.8 ether); | ||
|
||
feeRewardsManager.getEth(address(101)); | ||
|
||
// We've got the ether. | ||
assertEq(address(feeRewardsManager).balance, 0 ether); | ||
assertEq(address(101).balance, 2.8 ether); | ||
} | ||
|
||
function createWithdrawalSimulateRewards( | ||
address withdrawalCredential, | ||
uint reward | ||
) public returns (RewardsCollector) { | ||
address payable addr = feeRewardsManager.createFeeContract( | ||
withdrawalCredential | ||
); | ||
addr.transfer(reward); | ||
return RewardsCollector(payable(addr)); | ||
} | ||
|
||
function testGetMultipleRewards() public { | ||
address payable[] memory addrs = new address payable[](100); | ||
for (uint256 i = 0; i < 100; ++i) { | ||
addrs[i] = payable( | ||
address( | ||
createWithdrawalSimulateRewards( | ||
address(uint160(i + 100)), | ||
10 ether | ||
) | ||
) | ||
); | ||
} | ||
feeRewardsManager.batchCollectRewards(addrs); | ||
assertEq(address(feeRewardsManager).balance, 280 ether); | ||
} | ||
|
||
function testChangeDefaultFee() public { | ||
feeRewardsManager.changeDefaultFee(100); | ||
assertEq(feeRewardsManager.defaultFeeNumerator(), 100); | ||
|
||
address addr = address( | ||
createWithdrawalSimulateRewards(address(100), 10 ether) | ||
); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
assertEq(address(100).balance, 9.9 ether); | ||
// We receive 1%. | ||
assertEq(address(feeRewardsManager).balance, 0.1 ether); | ||
} | ||
|
||
function testChangeFee() public { | ||
address addr = address( | ||
createWithdrawalSimulateRewards(address(100), 10 ether) | ||
); | ||
feeRewardsManager.changeFee(payable(addr), 10000); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
assertEq(address(100).balance, 0 ether); | ||
// We receive 100%. | ||
assertEq(address(feeRewardsManager).balance, 10 ether); | ||
} | ||
|
||
function testZeroRewards() public { | ||
address addr = address( | ||
createWithdrawalSimulateRewards(address(100), 0) | ||
); | ||
vm.expectRevert("Nothing to distribute"); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
} | ||
|
||
function testFuzzyHappyPathNoContracts( | ||
uint128 rewards, | ||
address owner | ||
) public { | ||
// Some smart contracts will revert when called, avoid them. | ||
vm.assume(owner.code.length == 0); | ||
// Avoid some precompiles. | ||
vm.assume(owner > address(0x100)); | ||
vm.assume(rewards > 10000); | ||
vm.assume(rewards < 1e30); | ||
vm.deal(address(this), rewards); | ||
RewardsCollector collector = createWithdrawalSimulateRewards( | ||
owner, | ||
rewards | ||
); | ||
uint256 chorusAmount = (address(collector).balance * | ||
uint256(collector.feeNumerator())) / collector.FEE_DENOMINATOR(); | ||
uint256 withdrawalCredentialsAmount = address(collector).balance - | ||
chorusAmount; | ||
uint256 chorusBalanceBefore = address(feeRewardsManager).balance; | ||
uint256 withdrawalBalanceBefore = owner.balance; | ||
collector.collectRewards(); | ||
assertEq( | ||
address(owner).balance, | ||
withdrawalBalanceBefore + withdrawalCredentialsAmount | ||
); | ||
assertEq( | ||
address(feeRewardsManager).balance, | ||
chorusBalanceBefore + chorusAmount | ||
); | ||
} | ||
|
||
// Test calling `collectRewards` from a contract that calls `collectRewards` | ||
// again, this will revert as the Ether is divided just to Chorus and the | ||
// withdrawal credential. | ||
function testReentrantAttack() public { | ||
ReentrantAttack withdrawalCredentialContract = new ReentrantAttack(); | ||
address addr = address( | ||
createWithdrawalSimulateRewards( | ||
address(withdrawalCredentialContract), | ||
10 ether | ||
) | ||
); | ||
vm.expectRevert("Failed to send Ether back to withdrawal credential"); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
} | ||
|
||
function testSendToContractWithdrawalCredential() public { | ||
WithdrawalContract withdrawalCredentialContract = new WithdrawalContract(); | ||
address addr = address( | ||
createWithdrawalSimulateRewards( | ||
address(withdrawalCredentialContract), | ||
10 ether | ||
) | ||
); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
assertEq(address(feeRewardsManager).balance, 2.8 ether); | ||
assertEq(address(withdrawalCredentialContract).balance, 7.2 ether); | ||
} | ||
|
||
function testChangeOwnerWithContract() public { | ||
WithdrawalContract withdrawalCredentialContract = new WithdrawalContract(); | ||
address addr = address( | ||
createWithdrawalSimulateRewards( | ||
address(withdrawalCredentialContract), | ||
10 ether | ||
) | ||
); | ||
RewardsCollector(payable(addr)).collectRewards(); | ||
assertEq(address(feeRewardsManager).balance, 2.8 ether); | ||
assertEq(address(withdrawalCredentialContract).balance, 7.2 ether); | ||
} | ||
} | ||