Skip to content

Commit

Permalink
Add Giver
Browse files Browse the repository at this point in the history
  • Loading branch information
CodeSandwich committed May 27, 2024
1 parent ee32d03 commit 3d235fa
Show file tree
Hide file tree
Showing 2 changed files with 327 additions and 0 deletions.
151 changes: 151 additions & 0 deletions src/Giver.sol
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));
}
}
176 changes: 176 additions & 0 deletions test/Giver.t.sol
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);
}
}

0 comments on commit 3d235fa

Please sign in to comment.