-
Notifications
You must be signed in to change notification settings - Fork 20
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
1 parent
ee32d03
commit 3d235fa
Showing
2 changed files
with
327 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
pragma solidity ^0.8.20; | ||
|
||
import {AddressDriver, Drips, IERC20} from "./AddressDriver.sol"; | ||
import {Managed} from "./Managed.sol"; | ||
import {Clones} from "openzeppelin-contracts/proxy/Clones.sol"; | ||
import {Address} from "openzeppelin-contracts/utils/Address.sol"; | ||
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
/// @notice Each Drips account ID has a single `Giver` contract assigned to it, | ||
/// and each `Giver` has a single account ID assigned. | ||
/// Any ERC-20 tokens or native tokens sent to `Giver` will | ||
/// eventually be `give`n to the account assigned to it. | ||
/// This contract should never be called directly, it can only be called by its owner. | ||
/// For most practical purposes the address of a `Giver` should be treated like an EOA address. | ||
contract Giver { | ||
/// @notice The owner of this contract, allowed to call it. | ||
address public immutable owner = msg.sender; | ||
|
||
receive() external payable {} | ||
|
||
/// @notice Delegate call to another contract. This function is callable only by the owner. | ||
/// @param target The address to delegate to. | ||
/// @param data The calldata to use when delegating. | ||
/// @return ret The data returned from the delegation. | ||
function delegate(address target, bytes memory data) | ||
public | ||
payable | ||
returns (bytes memory ret) | ||
{ | ||
require(msg.sender == owner, "Caller is not the owner"); | ||
return Address.functionDelegateCall(target, data, "Giver failed"); | ||
} | ||
} | ||
|
||
/// @notice This contract deploys and calls `Giver` contracts. | ||
/// Each Drips account ID has a single `Giver` contract assigned to it, | ||
/// and each `Giver` has a single account ID assigned. | ||
/// Any ERC-20 tokens or native tokens sent to `Giver` will | ||
/// eventually be `give`n to the account assigned to it. | ||
contract GiversRegistry is Managed { | ||
/// @notice The ERC-20 contract used to wrap the native tokens before `give`ing. | ||
IERC20 public immutable nativeTokenWrapper; | ||
/// @notice The driver to use to `give`. | ||
AddressDriver public immutable addressDriver; | ||
/// @notice The `Drips` contract used by `addressDriver`. | ||
// slither-disable-next-line naming-convention | ||
Drips internal immutable _drips; | ||
/// @notice The maximum balance of each token that Drips can hold. | ||
// slither-disable-next-line naming-convention | ||
uint128 internal immutable _maxTotalBalance; | ||
|
||
/// @param addressDriver_ The driver to use to `give`. | ||
constructor(AddressDriver addressDriver_) { | ||
addressDriver = addressDriver_; | ||
_drips = addressDriver.drips(); | ||
_maxTotalBalance = _drips.MAX_TOTAL_BALANCE(); | ||
|
||
address nativeTokenWrapper_; | ||
if (block.chainid == 1 /* Mainnet */ ) { | ||
nativeTokenWrapper_ = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; | ||
} else if (block.chainid == 5 /* Goerli */ ) { | ||
nativeTokenWrapper_ = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; | ||
} else if (block.chainid == 11155111 /* Sepolia */ ) { | ||
nativeTokenWrapper_ = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; | ||
} else { | ||
nativeTokenWrapper_ = address(bytes20("native token wrapper")); | ||
} | ||
nativeTokenWrapper = IERC20(nativeTokenWrapper_); | ||
} | ||
|
||
/// @notice Initialize this instance of the contract. | ||
function initialize() public { | ||
if (!Address.isContract(_giverLogic(address(this)))) new Giver(); | ||
} | ||
|
||
/// @notice Calculate the address of the `Giver` assigned to the account ID. | ||
/// The `Giver` may not be deployed yet, but the tokens sent | ||
/// to its address will be `give`n when `give` is called. | ||
/// @param accountId The ID of the account to which the `Giver` is assigned. | ||
/// @return giver_ The address of the `Giver`. | ||
function giver(uint256 accountId) public view returns (address giver_) { | ||
return _giver(accountId, address(this)); | ||
} | ||
|
||
/// @notice Calculate the address of the `Giver` assigned to the account ID. | ||
/// @param accountId The ID of the account to which the `Giver` is assigned. | ||
/// @param deployer The address of the deployer of the `Giver` and its logic. | ||
/// @return giver_ The address of the `Giver`. | ||
function _giver(uint256 accountId, address deployer) internal pure returns (address giver_) { | ||
return | ||
Clones.predictDeterministicAddress(_giverLogic(deployer), bytes32(accountId), deployer); | ||
} | ||
|
||
/// @notice Calculate the address of the logic that is cloned for each `Giver`. | ||
/// @param deployer The address of the deployer of the `Giver` logic. | ||
/// @param giverLogic The address of the `Giver` logic. | ||
function _giverLogic(address deployer) internal pure returns (address giverLogic) { | ||
// The address is calculated assuming that the logic is the first contract | ||
// deployed by the instance of `GiversRegistry` using plain `CREATE`. | ||
bytes32 hash = keccak256(abi.encodePacked(hex"D694", deployer, hex"01")); | ||
return address(uint160(uint256(hash))); | ||
} | ||
|
||
/// @notice `give` to the account all the tokens held by the `Giver` assigned to that account. | ||
/// @param accountId The ID of the account to `give` tokens to. | ||
/// @param erc20 The token to `give` to the account. | ||
/// If it's the zero address, `Giver` wraps all the native tokens it holds using | ||
/// `nativeTokenWrapper`, and then `give`s to the account all the wrapped tokens it holds. | ||
/// @param amt The amount of tokens that were `give`n. | ||
function give(uint256 accountId, IERC20 erc20) public whenNotPaused returns (uint256 amt) { | ||
address giver_ = giver(accountId); | ||
if (!Address.isContract(giver_)) { | ||
// slither-disable-next-line unused-return | ||
Clones.cloneDeterministic(_giverLogic(address(this)), bytes32(accountId)); | ||
} | ||
bytes memory delegateCalldata = abi.encodeCall(this.giveImpl, (accountId, erc20)); | ||
bytes memory returned = Giver(payable(giver_)).delegate(implementation(), delegateCalldata); | ||
return abi.decode(returned, (uint256)); | ||
} | ||
|
||
/// @notice The delegation target for `Giver`. | ||
/// Only executable by `Giver` delegation and if `Giver` is called by its deployer. | ||
/// `give`s to the account all the tokens held by the `Giver` assigned to that account. | ||
/// @param accountId The ID of the account to which tokens should be `give`n. | ||
/// It must be the account assigned to the `Giver` on its deployment. | ||
/// @param erc20 The token to `give` to the account. | ||
/// If it's the zero address, wraps all the native tokens using | ||
/// `nativeTokenWrapper`, and then `give`s to the account all the wrapped tokens. | ||
/// @param amt The amount of tokens that were `give`n. | ||
function giveImpl(uint256 accountId, IERC20 erc20) public returns (uint256 amt) { | ||
// `address(this)` in this context should be the `Giver` clone contract. | ||
require(address(this) == _giver(accountId, msg.sender), "Caller is not GiversRegistry"); | ||
if (address(erc20) == address(0)) { | ||
erc20 = nativeTokenWrapper; | ||
// slither-disable-next-line unused-return | ||
Address.functionCallWithValue( | ||
address(erc20), "", address(this).balance, "Failed to wrap native tokens" | ||
); | ||
} | ||
(uint128 streamsBalance, uint128 splitsBalance) = _drips.balances(erc20); | ||
uint256 maxAmt = _maxTotalBalance - streamsBalance - splitsBalance; | ||
// The balance of the `Giver` clone contract. | ||
amt = erc20.balanceOf(address(this)); | ||
if (amt > maxAmt) amt = maxAmt; | ||
// slither-disable-next-line incorrect-equality | ||
if (amt == 0) return amt; | ||
SafeERC20.forceApprove(erc20, address(addressDriver), amt); | ||
addressDriver.give(accountId, erc20, uint128(amt)); | ||
} | ||
} |
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,176 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
pragma solidity ^0.8.20; | ||
|
||
import {Test} from "forge-std/Test.sol"; | ||
import {AddressDriver, Drips, IERC20, StreamReceiver} from "src/AddressDriver.sol"; | ||
import {Address, Giver, GiversRegistry} from "src/Giver.sol"; | ||
import {ManagedProxy} from "src/Managed.sol"; | ||
import { | ||
ERC20, | ||
ERC20PresetFixedSupply | ||
} from "openzeppelin-contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; | ||
|
||
contract NativeTokenWrapper is ERC20("Native token wrapper", "NTW") { | ||
receive() external payable { | ||
_mint(msg.sender, msg.value); | ||
} | ||
} | ||
|
||
contract Logic { | ||
function fun(uint256 arg) external payable returns (address, uint256, uint256) { | ||
return (address(this), arg, msg.value); | ||
} | ||
} | ||
|
||
contract GiverTest is Test { | ||
Giver internal giver = new Giver(); | ||
address internal logic = address(new Logic()); | ||
|
||
function testDelegate() public { | ||
uint256 arg = 1234; | ||
uint256 value = 5678; | ||
|
||
bytes memory returned = giver.delegate{value: value}(logic, abi.encodeCall(Logic.fun, arg)); | ||
|
||
(address thisAddr, uint256 receivedArg, uint256 receivedValue) = | ||
abi.decode(returned, (address, uint256, uint256)); | ||
assertEq(thisAddr, address(giver), "Invalid delegation context"); | ||
assertEq(receivedArg, arg, "Invalid argument"); | ||
assertEq(receivedValue, value, "Invalid value"); | ||
} | ||
|
||
function testDelegateRevertsForNonOwner() public { | ||
vm.prank(address(1234)); | ||
vm.expectRevert("Caller is not the owner"); | ||
giver.delegate(logic, ""); | ||
} | ||
|
||
function testTransferToGiver() public { | ||
uint256 amt = 123; | ||
payable(address(giver)).transfer(amt); | ||
assertEq(address(giver).balance, amt, "Invalid balance"); | ||
} | ||
} | ||
|
||
contract GiversRegistryTest is Test { | ||
Drips internal drips; | ||
AddressDriver internal addressDriver; | ||
IERC20 internal erc20; | ||
IERC20 internal nativeTokenWrapper; | ||
GiversRegistry internal giversRegistry; | ||
address internal admin = address(1); | ||
uint256 internal accountId; | ||
address payable internal giver; | ||
|
||
function setUp() public { | ||
Drips dripsLogic = new Drips(10); | ||
drips = Drips(address(new ManagedProxy(dripsLogic, admin))); | ||
drips.registerDriver(address(1)); | ||
AddressDriver addressDriverLogic = | ||
new AddressDriver(drips, address(0), drips.nextDriverId()); | ||
addressDriver = AddressDriver(address(new ManagedProxy(addressDriverLogic, admin))); | ||
drips.registerDriver(address(addressDriver)); | ||
|
||
GiversRegistry giversRegistryLogic = new GiversRegistry(addressDriver); | ||
giversRegistry = GiversRegistry(address(new ManagedProxy(giversRegistryLogic, admin))); | ||
giversRegistry.initialize(); | ||
nativeTokenWrapper = giversRegistry.nativeTokenWrapper(); | ||
vm.etch(address(nativeTokenWrapper), address(new NativeTokenWrapper()).code); | ||
accountId = 1234; | ||
giver = payable(giversRegistry.giver(accountId)); | ||
emit log_named_address("GIVER", giver); | ||
|
||
erc20 = new ERC20PresetFixedSupply("test", "test", type(uint136).max, address(this)); | ||
erc20.approve(address(addressDriver), type(uint256).max); | ||
} | ||
|
||
function give(uint256 amt) internal { | ||
give(amt, amt); | ||
} | ||
|
||
function give(uint256 amt, uint256 expectedGiven) internal { | ||
erc20.transfer(giver, amt); | ||
uint256 balanceBefore = erc20.balanceOf(giver); | ||
uint256 amtBefore = drips.splittable(accountId, erc20); | ||
|
||
giversRegistry.give(accountId, erc20); | ||
|
||
uint256 balanceAfter = erc20.balanceOf(giver); | ||
uint256 amtAfter = drips.splittable(accountId, erc20); | ||
assertEq(balanceAfter, balanceBefore - expectedGiven, "Invalid giver balance"); | ||
assertEq(amtAfter, amtBefore + expectedGiven, "Invalid given amount"); | ||
} | ||
|
||
function giveNative(uint256 amtNative, uint256 amtWrapped) internal { | ||
Address.sendValue(giver, amtNative); | ||
Address.sendValue(payable(address(nativeTokenWrapper)), amtWrapped); | ||
nativeTokenWrapper.transfer(giver, amtWrapped); | ||
|
||
uint256 balanceBefore = giver.balance + nativeTokenWrapper.balanceOf(giver); | ||
uint256 amtBefore = drips.splittable(accountId, nativeTokenWrapper); | ||
|
||
giversRegistry.give(accountId, IERC20(address(0))); | ||
|
||
uint256 balanceAfter = nativeTokenWrapper.balanceOf(giver); | ||
uint256 amtAfter = drips.splittable(accountId, nativeTokenWrapper); | ||
assertEq(giver.balance, 0, "Invalid giver native token balance"); | ||
uint256 expectedGiven = amtNative + amtWrapped; | ||
assertEq(balanceAfter, balanceBefore - expectedGiven, "Invalid giver balance"); | ||
assertEq(amtAfter, amtBefore + expectedGiven, "Invalid given amount"); | ||
} | ||
|
||
function testGive() public { | ||
give(5); | ||
} | ||
|
||
function testGiveZero() public { | ||
give(0); | ||
} | ||
|
||
function testGiveUsingDeployedGiver() public { | ||
give(1); | ||
give(5); | ||
} | ||
|
||
function testGiveMaxBalance() public { | ||
give(drips.MAX_TOTAL_BALANCE()); | ||
give(1, 0); | ||
} | ||
|
||
function testGiveOverMaxBalance() public { | ||
erc20.approve(address(addressDriver), 15); | ||
addressDriver.setStreams( | ||
erc20, new StreamReceiver[](0), 10, new StreamReceiver[](0), 0, 0, address(this) | ||
); | ||
addressDriver.give(0, erc20, 5); | ||
give(drips.MAX_TOTAL_BALANCE(), drips.MAX_TOTAL_BALANCE() - 15); | ||
} | ||
|
||
function testGiveNative() public { | ||
giveNative(10, 0); | ||
} | ||
|
||
function testGiveWrapped() public { | ||
giveNative(0, 5); | ||
} | ||
|
||
function testGiveNativeAndWrapped() public { | ||
giveNative(10, 5); | ||
} | ||
|
||
function testGiveZeroWrapped() public { | ||
giveNative(0, 0); | ||
} | ||
|
||
function testGiveCanBePaused() public { | ||
vm.prank(admin); | ||
giversRegistry.pause(); | ||
vm.expectRevert("Contract paused"); | ||
giversRegistry.give(accountId, erc20); | ||
} | ||
|
||
function testGiveImplReverts() public { | ||
vm.expectRevert("Caller is not GiversRegistry"); | ||
giversRegistry.giveImpl(accountId, erc20); | ||
} | ||
} |