Giter Club home page Giter Club logo

contracts's Introduction

Drips protocol V2 smart contracts

Drips is an EVM blockchain protocol for streaming and splitting ERC-20 tokens. See docs for a high-level introduction and documentation.

Organization of the repository

The main branch is always in-development, it contains the newest version of the contracts that may not be deployed anywhere.

The hashes of the git commits that were used for deployments can be found in the deployments directory, in the JSON files under the Commit hash key. These commits are also git tagged using the v2_<chain_name>_deploy naming scheme, e.g. v2_ethereum_deploy.

The deployed contracts for the old, v1 version of the protocol can be found on the v1 branch.

Development

Drips uses Foundry for development. You can install it using foundryup.

The codebase is statically checked with Slither version 0.9.5. Here are the installation instructions.

Format code

forge fmt

Run tests

forge test

Run Slither

slither .

Deployment

Deploy to a local testnet

Start a local testnet node and let it run in the background:

anvil

Set up environment variables. See instructions for public network deployment to see all the options. To automatically set bare minimum environment variables run:

source scripts/local-env.sh

Run deployment:

scripts/deploy.sh

Deploy to a public network

Set up environment variables controlling the deployment process:

# The RPC URL to use, e.g. `https://mainnet.infura.io/MY_INFURA_KEY`.
# Contracts will be deployed to whatever network that endpoint works in.
export ETH_RPC_URL="<URL>"

# Foundry wallet arguments. They will be passed to all commands needing signing.
# Examples:
# WALLET_ARGS="--interactive" - Open an interactive prompt to enter your private key.
# WALLET_ARGS="--private-key <RAW_PRIVATE_KEY>" - Use the provided private key.
# WALLET_ARGS="--mnemonic-path <PATH> --mnemonic-index <INDEX>" - Use the mnemonic file
# WALLET_ARGS="--keystore <PATH> --password <PASS>" - Use the keystore in the given folder or file.
# WALLET_ARGS="--ledger --mnemonic-derivation-path <PATH>" - Use a Ledger wallet using the HD path.
# WALLET_ARGS="--trezor --mnemonic-derivation-path <PATH>" - Use a Trezor wallet using the HD path.
# WALLET_ARGS="--from <ADDRESS>" - Use the Foundry sender account.
# For the full list check Foundry's documentation e.g. by running `cast wallet address --help`.
export WALLET_ARGS="<ARGS>"

# OPTIONAL
# The API key to use to submit contracts' code to Etherscan.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export ETHERSCAN_API_KEY="<KEY>"

# OPTIONAL
# If set, submits contracts' code to Sourcify.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export VERIFY_SOURCIFY=1

# OPTIONAL
# If set, submits contracts' code to Blockscout.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export VERIFY_BLOCKSCOUT=1

# OPTIONAL
# The JSON file to write deployment addresses to. Default is `./deployment_<NETWORK_NAME>.json`.
export DEPLOYMENT_JSON="<PATH>"

Set up environment variables configuring the deployed contracts:

# The salt used for the deployment of a DripsDeployer instance.
# For the final, official mainnet deployments use `DripsDeployer`.
# For test deployments use something else, e.g. `DripsDeployerTest1`.
export DRIPS_DEPLOYER_SALT="<SALT>"

# OPTIONAL
# Address of the deployed contracts admin to set. If not set, the deployer's wallet address is used.
export ADMIN="<ADDRESS>"

# OPTIONAL
# Cycle length  to use in `DRIPS_LOGIC` when it's deployed. If not set, 1 week is used.
export DRIPS_CYCLE_SECS="<SECONDS>"

# OPTIONAL
# Address of the Drips admin to set. If not set, `ADMIN` is used.
export DRIPS_ADMIN="<ADDRESS>"

# OPTIONAL
# Address of the AddressDriver admin to set. If not set, `ADMIN` is used.
export ADDRESS_DRIVER_ADMIN="<ADDRESS>"

# OPTIONAL
# Address of the NFTDriver admin to set. If not set, `ADMIN` is used.
export NFT_DRIVER_ADMIN="<ADDRESS>"

# OPTIONAL
# Address of the ImmutableSplitsDriver admin to set. If not set, `ADMIN` is used.
export IMMUTABLE_SPLITS_DRIVER_ADMIN="<ADDRESS>"

# OPTIONAL
# The address of the AnyApi operator. If not set, zero address is used.
export REPO_DRIVER_OPERATOR="<ADDRESS>"

# OPTIONAL
# The AnyApi job ID used for requesting account owner updates.
# If not set, a string of zeros is used.
export REPO_DRIVER_JOB_ID="<JOB_ID>"

# OPTIONAL
# The fee in Link for each account owner. If not set, `0` is used.
export REPO_DRIVER_FEE="<FEE>"

# OPTIONAL
# Address of the RepoDriver admin to set. If not set, `ADMIN` is used.
export REPO_DRIVER_ADMIN="<ADDRESS>"

Run deployment:

scripts/deploy.sh

Verify and publish source code of an existing deployment

The deployment may not be verified, either because the deployer chose to do that, or because the verification process failed, as it randomly happens. Verification can be done separately for any deployment as long as the repository is checked out at the right commit.

Set up environment variables controlling the deployment process:

# The RPC URL to use, e.g. `https://mainnet.infura.io/MY_INFURA_KEY`.
# Contracts will be deployed to whatever network that endpoint works in.
export ETH_RPC_URL="<URL>"

# OPTIONAL
# The API key to use to submit contracts' code to Etherscan.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export ETHERSCAN_API_KEY="<KEY>"

# OPTIONAL
# If set, submits contracts' code to Sourcify.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export VERIFY_SOURCIFY=1

# OPTIONAL
# If set, submits contracts' code to Blockscout.
# In case of deployments to networks other than Ethereum an appropriate equivalent service is used.
# If not set, contracts won't be verified.
export VERIFY_BLOCKSCOUT=1

At least one of ETHERSCAN_API_KEY, VERIFY_SOURCIFY and VERIFY_BLOCKSCOUT must be set.

Run verification:

scripts/verify.sh <DRIPS_DEPLOYER>

The DRIPS_DEPLOYER parameter is the DripsDeployer contract address, it can be found in the deployment JSON.

contracts's People

Contributors

cloudhead avatar codesandwich avatar d-xo avatar efstajas avatar mrchico avatar shaheenrehman avatar xmxanuel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

contracts's Issues

Add key rotation

We currently don't have a way to rotate keys. This is important, because if you rotate your wallet keys and all your senders don't update their configurations, they'll keep sending to an old address. You'll be forced to use the old key to collect in its name and if that old key is ever compromised, an attacker can perpetually steal your funds as long as you don't ask every single of your senders to update their configuration.

The solution is to add a function which when called using the old key will:

  • withdraw all unsent funds
  • make you drip 100% of collected funds to your new address
  • block any future updates of your sender or drips configuration, probably by damaging the on-chain configuration hashes

This way even if the old keys are compromised, funds sent to the old address are guaranteed to eventually reach your new address.

This process must be repeated for each new key rotation. This has the downside that the cost of collecting all past keys grows linearly with each rotation. For frequent rotations it's probably best to set up some kind of a proxy contract, which power users will probably do anyway.

Move _balanceAt() methods to be adjacent in Drips.sol

A bit of a random request, but noticed as I was going through the code.

There are two _balanceAt() methods with different signatures in Drips.sol. They are located at different places in the file. I think it might make more sense to have them to be next to each other for simplicity. WDYT?

https://github.com/radicle-dev/drips-contracts/blob/8ca1b18833895c0940b829956e5f5938208a886a/src/Drips.sol#L484
https://github.com/radicle-dev/drips-contracts/blob/8ca1b18833895c0940b829956e5f5938208a886a/src/Drips.sol#L693

Explore incentives to push splits

0xsplits protocol has a nice feature of tipping the person who triggers splitting and pays the gas. They've implemented it as a configurable percentage of the split funds which goes to the caller. This way the network keeps flushing itself, especially during low gas price periods. If there are bots involved, we can expect the tip for the caller to be around the lowest possible gas cost to push the split. The regular splits receivers don't really lose anything, somebody needed to pay for the gas anyway. It's even better, because it's done optimally and in form of a tax proportionally divided between all the receivers. E.g. if there are 100 receivers and each gets just 50$, nobody will ever trigger splitting, it makes no financial sense. But if the caller tip is 10%, each will get only 45$ but the bot may actually make it a reality because it'll get 500$ which is enough to cover the gas.

An approach like this is worth exploring, either by copying the 0xsplits solution or by inventing something similar but better suited for us.

Globally deduplicate *ReceiverSeen events

The problem

When multiple users have identical drips or splits configurations, DripsReceiverSeen and SplitsReceiverSeen events are emitted for each of them, which wastes gas and causes noise in events.

The solution

The simplest approach is to add to storage a mapping(bytes32 => bool) seenHashes. Whenever a drips configuration is configured, check the value of seenHashes[configHash]. If it's true, do not emit the *ReceiverSeen events. If it's false, set it to true and emit the events.

The advantages

  • If a configuration contains just 2 or more items and it has been used before, gas is being saved. Not emitting the events saves more gas than costs reading the content of seenHashes.
  • In setSplits if a configuration has been seen before, we can assume that it's valid and we don't need to analyze it.
  • Additional data can be store in seenHashes. For splits it can be the sum of the weights, this way function Splits.splitResults doesn't need to accept currReceivers anymore and by extension neither does DripsHub.collectableAll.
  • Consumers of events don't need to perform any deduplication.

The disadvantages

  • If a configuration hasn't been seen before, it costs 22K gas to set the value of seenHashes. This cost may never be recouped if the configuration is short or not shared by many users.

Add drips with optional durations

Currently drips with duration are guaranteed to cover exactly that duration, which requires having balance high enough to cover them in full. This requirement is too hard for many use cases and we need to support drips with optional durations. Such drips last for a given duration OR until funds run out. They don't require balance to cover them in full.

An open question is whether we need drips with a hard duration at all. They provide a unique guarantee not expressable with optional durations that a drip will be executed even if a drips configuration is not maintained or topped up ever again.

Switch from Solhint to Slither

solhint isn't a very good linter anymore:

  • It's not very helpful. It very rarely finds anything and I can't recall a single time when its warning actually improved the code quality, these were always annoying nitpicks which forced flagging code to be ignored.
  • It's basically abandoned. The repository hasn't been pushed to in over a year and minimalistic Solidity-bumping updates are released by a single person without access to the upstream.

Slither seems to be an alternative worth trying out. It's actively developed and seems to be widely praised among developers.

Add start/end date to One-Time Fundraiser

Proposal

Allow for a start/end date for One-Time Fundraisers (i.e. gives)

A start/end date essentially adds a time limit.

Could add to the end of the UI like so:
image

Motivation

This would unlock a number of use cases, some with very IRL implications.

Time-Limited Fundraiser/Charity

Similar to GoFundMe

Workflow:

  • Start a charity drive with One-Time Fundraiser
  • Name it "Product Launch Fundraiser"
  • Set time limit for 3 months

Consideration: in the case of fundraiser/charity, it may make sense to have a refund mechanism, such that if the goal of X amount is not met in Y time limit, 100% of funds are sent back to givers.

IRL Events

Similar to TicketMaster, EventBrite, StubHub, etc.

Workflow:

  • Start a community with One-Time Fundraiser
  • Name it "Eth Denver After Party - Feb 21"
  • I have a start date of today
  • I have an end date of Feb 21
  • I set the minimum price to 50DAI (~$50 per "ticket")
  • I set the NFT limit to 1,000 (= 1,000 tickets for entry)

Consideration: this could be used for any kind of event; music, conference, art exhibit, etc.

Drop Receiver.lastFundsPerCycle

Instead of saving last funds per cycle in Receiver.lastFundsPerCycle we can add it to Receiver.amtDeltas[nextCollectedCycle].thisCycle. This should reduce gas usage, because it requires maintaining less variables in storage.

Add scheduled drips

It may be possible to support drips configurations with optional start and end time. The user would need to have balance covering at least all the drips with end times. Drips without end time probably should run for as long as balance allows.

Partially done in #84, still to do:

  • restore events emitted when setting drips
  • add tests for the scheduled drips

Pack the drips parameters

The current situation

Currently the DripsReceiver parameter is defined as:

struct DripsReceiver {
    uint256 userId;
    uint128 amtPerSec;
    uint32 start;
    uint32 duration;
}

And DripsReceiverSeen event as:

event DripsReceiverSeen(
    bytes32 indexed receiversHash,
    uint256 indexed userId,
    uint128 amtPerSec,
    uint32 start,
    uint32 duration
);

The proposal

Pack all 3 of amtPerSec, start and duration into a single uint256 word as

uint256 dripParams = amtPerSec << 64 | start << 32 | duration;

Technically we use a User Defined Value Type for this.

The advantages

  • Lower events gas cost. Each emitted DripsReceiverSeen has gas usage reduced by 512, from 1893 to 1381

  • Lower calldata gas cost. Each DripsReceiver in calldata has its maximum gas usage reduced by 256, from 1184 to 928. Additionally there's less to pass around in memory and hash. Bit-shifting is very cheap, so accessing fields is marginally more expensive.

  • Easier sorting of the receivers list. Instead of having a 4-step comparison (userId => amtPerSec => start => duration) we have a 2-step (userId => dripParams).

The disadvantages

  • Harder to work with API, requires packing and unpacking parameters.

Explore switching most of local variables to 256-bit representations

Most of the logic in Drips and Splits operates on integers as small as possible. The tight control was supposed to guard against overflows in the logic by presenting that they're impossible. Unfortunately it seems to introduce some gas overhead in the logic (still needs to be measured) and a lot of noise with castings all over the place. It's also worth analyzing which parts of the API actually benefit from the smaller integers and which would be better off with 256-bit variants everywhere.

Add vesting with an optional cliff

Some DAOs vest their contributor payments over a certain timeframe or use what is called a cliff, which essentially allows a contributor to claim more of their vested tokens over time.

Scenario: Contributor requests 15k Float a month. The DAO does not know the contributor but if the contributor ships what was promised, they would be willing to pay that compensation.
They use the drips hub to establish a stream with a cliff which in the first months gives access to 6k, then in the second month he gets access to 7k and so on. The remaining compensation (i.e. 15k - 6k for the first month, gets payed after the 15k were reached in about 9 months).

Support low-decimals tokens

Some tokens have very few decimals and it impacts the precision of dripping. E.g. USDC has only 6 decimals which leads to the minimum possible dripping rate of 2.629743 USDC per month, which is very impractical.

One solution is what Llama pay does: force every token to have additional 20 decimals. For our codebase it's infeasible, because this will make amounts of tokens with an already high precision and high supply not fit in 128-bit integers.

Add a treasury

The funds currently locked in the pool aren't used for anything. We may be able to automatically invest them if it's safe, e.g. in DSR. The first step in this experiment is to add a simple treasury, which as of now won't do anything except keeping the funds.

Emit an event for the splits receiver when splits configured

As of now when setting splits, a SplitsUpdated event is emitted which contains an unindexed list of receivers. Without an aggregator like a subgraph it's impossible for the receiver to detect this event and start monitoring the splitting user. Only if they decide to collect a Split event is emitted which can be noticed by the receiver. Until then there could be a hidden fortune of assets waiting to be split if only the receiver knew about its existence.

It would probably make sense to introduce a new event type:

event Splitting(address indexed user, address indexed receiver, uint32 weight);

A series of events like this may or may not be enough to take over the function of a SplitsUpdated event. If that's the case, SplitsUpdated could be removed to save gas. If not, user probably doesn't need to be indexed.

Applying to a workstream without changing the total incorrectly proposes fraction of original workstream total

To reproduce:

  1. Create a workstream with a total of 120.000 DAI / 1 year
  2. Switch account to any other
  3. Click "Apply" on the created workstream created in step 1
  4. Observe the workstream totals pre-populate the apply modal with "120,000" as the total, and "365 days" as the duration
  5. Apply without changing the total
  6. Refresh page

Expectation: Application is made with a total of 120000 DAI / 365 days
Reality: Application is made with a counteroffer for a total of 120 DAI / 365 days

Stop allowing anybody to collect for anybody else in `AddressApp` and accept the address to send funds to.

In the current design the funds are forced to be collected into the address which the user ID is based on which is very inflexible. Also the timing of the transfers may be unexpected for the user.

We should stop allowing anybody to call collect for anybody else in AddressApp. We should also allow passing the address to send funds to.

This may be useful with the introduction of Caller, because it allows sending funds to a smart contract, which may then perform some actions with the collected tokens. It also may work well with cold wallets authorizing other addresses to perform calls on their behalf.

Remove collectAll from DripsHub (TBD?)

collectAll is a nice helper if it's called directly by an EOA. DripsHub's collectAll is a function callable only by apps, it doesn't provide much value in that context, it's an API bloat. If an app wants to mimic collectAll, it can reimplement it on its side, it's just 3 lines of code. Even on the app side it may not be needed if transactions batching is implemented.

ERC20Reserve rescue functionality

Currently it is not possible to rescue erc20 tokens from the reserve contract if some - other than the registered token - have been send to the contract by accident.

If this is a concern I would be happy to push a pr.

Switch from `prettier-plugin-solidity` to `forge fmt`

forge fmt is getting more and more mature. It still has a few wrinkles, but generally it already works fine. It's much faster than prettier and it doesn't require separate installation along Node.js. prettier-plugin-solidity also isn't maintained very well, it tends to lag when newer versions of Solidity are released.

Prevent two ERC-20 transfers in updateSender

Currently if the user tops up and collects in updateSender, a full top-up is first transferred to the pool and then the collected funds are transferred back. This can be merged into a single transfer.

Turn Drips and Splits into contracts mimicking libraries

Splitting of DripsHub into Drips and Splits libraries seems to be a good decision driven by a plan to create building blocks for different protocols. Unfortunately the decision to wrap Drips and Splits logic into libraries comes with some annoyances and gas waste:

  • We need to pass around storage pointers
  • We need to pass around immutable variables and watch out not to mutate them in the process
  • Events from libraries don't show up in DripsHub's ABI without manually copying them
  • We don't benefit from the libraries' dynamic linking

The proposed solution is to turn Drips and Splits into regular contracts. They should keep being 100% hidden from DripsHub's API by having only internal and private members, after all they don't define an API, they're purely toolkits for building protocols.

Migrating to forge

Foundry, especially forge provides helpful cheat-codes that can be utilized for more detailed testing.
Examples would be expectRevert() and expectEmit() for custom error and event tests.

In case I can convince you to migrate, I would be open to push a pr.

Add NFT-based identities

Add NFTDriver for user authentication. Anybody can mint as many as they want. The API is very generic, similar to AddressDriver. The authentication is performed by checking if the given token is owned by the message sender OR the message sender is approved for that token (in the end that's equivalent, an approved address can just transfer the token to themselves, perform an action on DripsHub and then transfer it back).

Add squeezing drips for the current cycle

We want to better support use cases when there are few large drips incoming. For cases like this waiting for a cycle to end in order to receive funds may be a large problem.

A potential workaround is to allow receiving drips from users individually without waiting for the cycle end. Below is a proposal for implementation of a system like this. We've looked at a few approaches and this one seems the simplest while still being useful.

Implementation

Add to stored per-user drips state a mapping:

struct DripsState {
    ...
    mapping(uint256 => Squeezable) squeezables; // The key is the dripping user's ID
}

struct Squeezable {
    uint32 fromTimestamp; // A timestamp from which funds can be squeezed using the current receivers list
    uint128 amt; // An additional amount squeezable for `fromTimestamp`'s cycle
}

Setting drips

When drips between Alice and Bob are updated, a few additional operations are done:

Squeezable storage squeezable = dripsStates[Bob].squeezables[Alice];
// New cycle, clear the old `amt`, it can't be squeezed
if(_cycleOf(squeezable.fromTimestamp) != _cycleOf(block.timestamp) squeezable.amt = 0;
// Calculate amount dripped in the current cycle according to the old configuration which hasn't been squeezed yet
uint32 start = max(squeezable.fromTimestamp, _cycleOf(block.timestamp) * cycleSecs);
squeezable.amt += (block.timestamp - start) * oldAmtPerSec; 
squeezable.fromTimestamp = block.timestamp;

This operation means an extra slot written to. On the first drips setup between Alice to Bob Alice will pay extra 22K gas, on consecutive 5K.

Receiving

receiveDrips gets an extra argument:

function receiveDrips(..., Squeezed[] memory squeeze) ...

struct Squeezed {
    uint256 userId;
    DripsReceiver[] memory receivers;
}

squeeze can be empty, then receiving is unchanged. If it isn't, for each squeezed user their receivers list is verified against their on-chain hash. Then the receivers list is searched for the receiving user and for each matching entry the dripped amount is calculated for time range

max(squeezable.fromTimestamp, _cycleOf(block.timestamp) * cycleSecs) => block.timestamp

These amounts are summed up and added to

_cycleOf(squeezable.fromTimestamp) == _cycleOf(block.timestamp) ? squeezable.amt : 0;

That amount is considered received. To prevent double receiving, amtDeltas for the currently running cycle must be reduced by the squeezed amount. It's safe, because the squeezed amount is at most the amount dripped in the current cycle up to the current timestamp.

Finally,squeezables is cleaned up:

squeezable.amt = 0;
squeezable.fromTimestamp = block.timestamp`;

Move more data off-chain and include it in the hash

The benefits:

  • less storage used
  • expensive calculations can be cached

The disadvantages:

  • The API gets more difficult to use
  • The on-chain pool users (e.g. funding NFTs) need to store that data somewhere, either on-chain (expensive) or in the events

Some candidates:

  • sender start balance
  • sender start time
  • sender drips configuration
  • sum of receivers weights

Globally limit the amount of each token locked in DripsHub

From DripsHub documentation:

/// The contract assumes that all amounts in the system can be stored in signed 128-bit integers.
/// It's guaranteed to be safe only when working with assets with supply lower than `2 ^ 127`.

This is rather difficult to verify as long as DripsHub is supposed to run on any ERC-20s. We can add a locked funds counter in DripsHub and revert if it exceeds 2 ^ 127. These are the effects:

  • (good) We protect ourselves from all kinds of lock-ups coming from overflowing math
  • (good) We can ditch most of overflow checks
  • (bad) We need a new storage slot per token, which must be updated in any transaction transferring funds in or out of DripsHub

An alternative solution saving some gas would be to rely on the balance of the reserve, but that's probably dangerous to base protocol's math soundness on a treasury balance.

Squeezing can be vandalized

As of now anybody can squeeze for anybody else and there's nothing stopping a malicious user from squeezing using a history containing only the latest configuration. If there are more than one history entries which can be squeezed, that can lead to making funds unsqueezeable and locked until the cycle ends.

Some solutions:

  • Ban squeezing for 3rd parties altogether. This reduces the robustness of the ecosystem.
  • Implement ACL of who can squeeze. It's complicated and requires involvement.
  • Ban skipping history entries except the latest ones. This forces everybody to cover the cost of squeezing the entire history for the current cycle whether it makes a financial sense or not.
  • Same as above, but not for the squeezed user themselves. This bans malicious actions, but still reduces the efficiency of squeezing. It also increases the API complexity.
  • Keep track of which configurations have been squeezed in the current cycle. It may be enough to store a bit map with flags. It greatly increases flexibility without making the usage much more complex, but it may be technically challenging.

Store the last drips balance and update timestamp on-chain

Currently the last drips balance and the last drips update timestamp are passed around as calldata. The user of the contract is expected to retrieve it from the last event and pass it in the next setDrips call. I think that we should consider keeping these parameters in storage on-chain.

This would change the drips storage from this:

struct DripsState {
    // Slot 1
    bytes32 dripsHash;
    // Slot 2
    uint64 nextCollectedCycle;
    // Slot 3
    mapping(uint64 => AmtDelta) amtDeltas;
}

to this:

struct DripsState {
    // Slot 1
    bytes32 dripsHash;
    // Slot 2
    uint64 nextCollectedCycle;
    uint128 balance;
    uint64 updated;
    // Slot 3
    mapping(uint64 => AmtDelta) amtDeltas;
}

Pros:

  • HIGH LEVEL Easier to use API. These parameters have always been causing confusion.
  • HIGH LEVEL We gain the possibility of allowing anybody to top-up anybody else's drips balance. We may not choose to do that in DripsHub, but other contracts based on Drips lib could allow that. In the current implementation this is impossible because any top-up changes the drips state hash, so every call to setDrips could be front-run and rendered invalid by a top-up call.
  • Less transaction calldata and less data passed between contracts. This saves some gas, in top-level TX calldata around 400, I'm not sure how much in in internal TXs.
  • HIGH LEVEL In case there's no dripsHash change, there's no need to emit the full drips configuration in the events, the old ones can be easily reused. This is important because events are quite expensive. This could be done in the current implementation, but would require decoupling the drips state hash and the drips configuration hash, that would be complicated to use.
  • If #122 is merged, we may cache on-chain defaultEnd without any additional cost, because the slot with balance and updated needs to be updated anyway.

Whatevers:

  • More stable state hashes. Some transactions like drips top-up don't cause the hash state change anymore. In such cases we don't need to update the on-chain dripsHash, only the balance and updated, so still 1 slot.
  • The cost of initializing the user's slot with nextCollectedCycle, balance and updated is shared between the user themselves and the first user to drip to them. Globally it doesn't increase the cost.

Cons:

  • The drips state now takes 2 slots instead of 1. Changes with reconfiguration of drips require an update to 1 more slot.

WDYT? @xmxanuel but from a higher level (marked as HIGH LEVEL) also @gh0stwheel and @evvvritt

Note to the implementor: it'll make even more sense to add a function for querying the current drips balance

Drips hub 2.0

This is a meta-issue for the new version of drips hub.

As of now the large features we know we want to deliver are:

The ever-evolving list of steps to achieve this is:

  • Add support for multiple tokens to reserve
  • Add support for multiple tokens to hub, stop setSplits from collecting
  • Split collect into split and collect. collect is callable only by the collected address itself.
  • Allow accounts receiving funds
  • Add EOA driver as seperate contract
  • ENS Driver (optional)
  • Drop support for plain address identities in hub
  • Add reserve plugins (like DSR saving rates, etc for specific tokens)

Extract DripsLib and SplitsLib

We have a high-level goal of being able to turn drips and splits into independent protocols. This can be done by refactoring the codebase into:

  • DripsLib - a library with the drips logic. It works on abstract user IDs, asset IDs and amounts. It exposes only low-level dripping API and performs no user verification and no actual transfers. It only modifies the storage, emits events and tells the caller what are the side-effects to apply.
  • SplitsLib - similar to DripsLib but implementing splits logic.
  • Managed - an abstract contract implementing ownership and upgradability. It provides tools for making functions pausable and, if possible in a generic way, for storing data in upgrade-safe slots. It doesn't expose any drips-specific APIs and doesn't interact with drips or splits.
  • ERC20DripsHub - a concrete contract implementing the drips-specific API. It defines user authorization scheme and what the abstract userID or assetID actually mean. It talks to the reserve to perform token transfers. It ties together and drives the libs. It inherits Managed and uses it to make functions pausable and to safely store data for DripsLib, SplitsLib and for itself.

In case one wants to define their own protocol, they can wrap any elements of that stack in a custom contract with a new API tuned for a different use case.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.