Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP-5075: RateLimit, an outflow limiter for assets #5075

Closed
wants to merge 45 commits into from
Closed
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0261f0a
Rate Limiter for Token Outflows from Contracts
pr0toshi May 5, 2022
4e0deaa
Update eip-transfer_rate_limit
pr0toshi May 5, 2022
7e1f0ea
Update EIPS/eip-transfer_rate_limit
pr0toshi May 6, 2022
829dfe8
Update eip-transfer_rate_limit
pr0toshi May 6, 2022
8bcb271
Update EIPS/eip-transfer_rate_limit
pr0toshi May 7, 2022
a52ce67
Rename EIPS/eip-transfer_rate_limit to EIPS/EIPs/eip-5075.md
pr0toshi May 7, 2022
e4ed10d
Update eip-5075.md
pr0toshi May 7, 2022
923e9cb
Update eip-5075.md
pr0toshi May 7, 2022
64b54b3
Create eip-5075.md
pr0toshi May 7, 2022
d659953
Update eip-5075.md
pr0toshi May 7, 2022
629d181
Update eip-5075.md
pr0toshi May 7, 2022
a161734
Update eip-5075.md
pr0toshi May 7, 2022
9528bcf
Update eip-5075.md
richance May 8, 2022
3e0f8ca
Merge pull request #1 from richance/patch-2
pr0toshi May 8, 2022
a2aa3ed
Update eip-5075.md
pr0toshi May 8, 2022
16b66dc
Update eip-5075.md
pr0toshi May 8, 2022
731f021
Update eip-5075.md
pr0toshi May 8, 2022
630a1aa
Update eip-5075.md
pr0toshi May 8, 2022
1711b11
Update eip-5075.md
pr0toshi May 10, 2022
ff82a98
Update eip-5075.md
pr0toshi May 10, 2022
7a78f20
Update eip-5075.md
pr0toshi May 10, 2022
a3a1878
Update EIPS/EIPs/eip-5075.md
pr0toshi May 26, 2022
4b5a4db
Update eip-5075.md
pr0toshi May 26, 2022
37b077c
Create eip-5075.md
pr0toshi May 26, 2022
188f7cc
Update EIPS/EIPs/eip-5075.md
pr0toshi May 26, 2022
6f0ac6d
Update EIPS/EIPs/eip-5075.md
pr0toshi May 26, 2022
fa8d766
Update eip-5075.md
pr0toshi May 26, 2022
5b9bf4a
Update eip-5075.md
pr0toshi May 26, 2022
158f0c0
Update EIPS/EIPs/eip-5075.md
pr0toshi May 31, 2022
7f923fa
Update eip-5075.md
pr0toshi May 31, 2022
3d91b0d
fix issue with semicolon
lightclient May 31, 2022
ac23314
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
987bdf3
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
5d1a8ea
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
0dce148
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
9f06d0d
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
6f2d816
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
c35a3a9
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
76d8e68
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
c46e490
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
7c7fec6
Update EIPS/EIPs/eip-5075.md
pr0toshi Jul 26, 2022
16cbbfb
Rename EIPS/EIPs/eip-5075.md to EIPS/eip-5075.md
MicahZoltu Jul 27, 2022
bc0a53a
No colons in title
Pandapip1 Sep 1, 2022
d396ac9
Merge branch 'master' into patch-1
Pandapip1 Sep 1, 2022
fcb882e
Apply suggestions from code review
Pandapip1 Sep 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 302 additions & 0 deletions EIPS/eip-5075.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
---
eip: 5075
title: RateLimit, an outflow limiter for assets
description: Limits outflows for all contract assets to a given rate in a given timeframe
author: PR0 (@pr0toshi)
discussions-to: https://ethereum-magicians.org/t/eip-x-limit-token-outflows-within-timeframe-to-limit-hack-losses/9172
status: Draft
type: Standards Track
category: ERC
created: 2022-05-05
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
created: 2022-05-05
created: 2022-05-05
requires: 20

Is there a dependency on ERC-20 from this EIP?

---

## Abstract
An inheritable implementation for a rate limiter using a new `transferL` function which replaces all asset outflow transfer functions to unknown external addresses. By limiting asset outflows for all assets in a contract within a customizable timeframe, contracts that suffer from hacks will have losses limited to the given `rateLimit` parameter, which is dynamic and relative to a contract's net total assets, being tracked at the per asset level.


Limiting outflows within given timeframes to external addresses stops both single transaction vulnerabilities which drain all funds, as well as those which are not single transaction/flash loan based and may be performed throughout a given timeframe. By delaying and limiting outflows, teams will have time to respond with the security mechanisms in contracts determined by the teams (upgrades, freezes, not rate limited `onlyOwner` multisig based withdrawals), limiting losses by users.

This EIP includes an optional whitelist functionality.


## Motivation
The cryptocurrency ecosystem continues to suffer from record-setting losses due to attacks, often using single or multi-transaction exploits (with flash loans, for example), to rapidly withdraw more funds than intended. While there are often mechanisms implemented to freeze a contract in the aftermath of an attack, these types of attacks cannot be limited or stopped in real-time or mid-transaction.

This EIP creates a simple, plug-and-play layer of protection that limits outflows to the intended expected limits, and reverts should these limits be exceeded. This will limit the extent of the damage in the event of a fund-draining vulnerability. The function has been optimized to have minimal overhead on transfers and is intended to replace transfer functions throughout contracts.

## Specification
### Methods

### transferL

Transfers _amount of tokens to address _to, and SHOULD limit the _amount to less than the


```
rateLimit * contract token balance - (token outflow - contract balance * rateLimit / (timeLimit / (current timestamp - last outflow timestamp)).
```
Implementations MAY use alternative ways to limit outflow within time frames. Some alternatives may be taking the usual volume and limit to 2 std, or may have some other parameters around influencing the limit for outflows. The spirit around having a final fallback plan for fns which allow any outflows post hack only being able to transfer out a limited amount vs full balance to protect the contract should there be a vulnerability, which unlocks the ability to withdraw more than intended should be the core concept taken away rather than a specific implementation or expectation on those limitations.


The function MUST throw or revert if the amount would be higher than the allowed rateLimit within the last timeLimit timeframe.

Note Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event as well as MUST update the token outflow based on the time passed since last outflow.

Note Transfers from specified addresses MAY skip the outflow and rate limit checks, with the addresses being able to be updated by owner or some other mechanism.

Contracts SHALL replace all ETH and Token based transfers with transferL where the transfers are to unknown external addresses or are publicly accessible.

```
function transferL(address _to, uint256 _amount, address _token) public returns (bool success)
```


### getLimitLeft


Returns the amount allowed within the rate limited transfer allowance for the current time with current outflow.

```
function getLimitLeft(address token) public view returns (uint256 rateLeft)
```


### whitelist


Allows an address or addresses to whitelist other addresses as msg.sender to not be rate limited.
OPTIONAL - This allows for an owner or other mechanism to allow contracts such as YEARN to be able to move positions or manage positions without affecting or being affected by the rate limit and out flows. Use with caution and for contracts with known deployed bytecode and that have been verified to not be malicious or able to be maliciously used to allow for getting around the rate limit during hacks. Note remember to restrict the address able to use the whitelist and removeWhitelist fn.

```
function whitelist(address addrs) public
```

```
function removeWhitelist(address addrs) public
```


## Rationale
By limiting token outflows at a per token level, malicious interactions can be limited in their damage as all net outflows are tracked and limited for all users based on a realtime allowance based on acceptable outflow relative to total amount held and within a given timeframe. The decision to track at the token level was for simplicity, gas efficiency and not needing to itself have vulnerabilities with oracles and accounting to track as a pool vs per asset. Whitelisting allows for contracts like yearn to be compatible if the destination is a known address not likely to be used in an attack. Dynanmic rate limits will help not stop but limit the possible damage, as all outflows need to use a transfer fn to steal assets, which this stops if reaches the rate limit.


## Backwards Compatibility
This will be non backwards compatible with contracts that are NOT whitelisted which assume they can just withdraw large positions, if those positions are large relative to the target contracts net position by all users. Should this be the case the contract can be fully compatible by simply being whitelisted, skipping all rate limit checks. There should be no other problems with compatibility as uses the standard ERC20 interface for tokens and usual ether transfer to replace.


## Reference Implementation

```
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8;

import"IERC20.sol";

contract rateLimit {
mapping(address => TokenInfo) public infoL;
struct TokenInfo {
uint32 timestamp; //tracks most recent send
uint224 ratedFlow; //tracks amount sent for the last timeWindow time
}

uint256 public timeWindow = 3600; //how long in seconds to limit within, recommend 1h = 3600
uint16 public rateLimit = 1000; //the basis points (00.0x%) to allow as the max sent within the last timeWindow time

mapping(address => uint8) public whitelisted; //stores whitelisted addresses to not be rate limited, useful for yearn style or deposit contracts
address public authAddress; //allows a set address that can whitelist addresses

//updates the time as well as the relative amount in basis points to track and rate limit outflows in which to track
//_rateLimit = 00.x%, _timeWindow = time in seconds
function updateLimits(uint16 _rateLimit, uint256 _timeWindow) internal {
rateLimit = _rateLimit;
timeWindow = _timeWindow;
}

//change the auth address that can whitelist addresses
function changeAuthAddress(address newAddrs) public {
require(msg.sender == authAddress);
authAddress = newAddrs;
}

//whitelist addresses as the msg.sender to not be rate limited
function whitelist(address addrs) public {
require(msg.sender == authAddress);
whitelisted[addrs] = 1;
}
//removes whitelist addresses as the msg.sender to not be rate limited
function removeWhitelist(address addrs) public {
require(msg.sender == authAddress);
whitelisted[addrs] = 0;
}

//gets the token amount left to be within the allowable limit
function getLimitLeft(address token)
public
view
returns (uint256 rateLeft)
{
TokenInfo storage info = infoL[token];
uint256 ratedFlow;
//if outside the time dwindow
if (timeWindow <= block.timestamp - info.timestamp) {
return (0);
}
//if the last transaction was within the time window, decreases the tracked outflow rate relative to the time elapsed, so that the limit is able to update in realtime rather than in blocks, making flows smooth, and increasing the rate available as time increases without a transaction
else {
unchecked {
ratedFlow = info.ratedFlow;
uint256 limitUnlocked;

if(block.timestamp != info.timestamp)
{
if (token==address(0)) {
limitUnlocked = uint224(
(address(this).balance * rateLimit) /
(timeWindow / (block.timestamp - info.timestamp)) /
10000
);
}
else {
limitUnlocked = uint224(
IERC20(token).balanceOf(address(this)) * rateLimit /
(timeWindow / (block.timestamp - info.timestamp)) /
10000
);
}
}
if (ratedFlow <= limitUnlocked) {
return 0;
} else {
return ratedFlow - limitUnlocked;
}
}
}
}

//used to replace ERC20 erc20token.transfer() and ETH address.transfer() fns in all public accessible fns which change the balances for the contract as outflow transactions.
//to used for the recipient address, amount for value amount in raw value, token as the tokens contract address to check, for ETH use address(0x0)
function transferL(
address to,
uint256 amount,
address token
) public returns (bool success) {
TokenInfo storage info = infoL[token];

if (whitelisted[msg.sender] == 1) {
if (address(token) == address(0)) {
payable(to).transfer(amount);
return(true);
} else {
return(IERC20(token).transfer(to, amount));
}
}
//used if the asset is ETH and not an ERC20 token
else {
if (address(token) == address(0)) {
unchecked {
//if the last transaction was within the time window, decreases the tracked outflow rate relative to the time elapsed, so that the limit is able to update in realtime rather than in blocks, making flows smooth, and increasing the rate available as time increases without a transaction
uint256 limitUnlocked;

if(block.timestamp != info.timestamp)
{
limitUnlocked = uint224(
(address(this).balance * rateLimit) /
(timeWindow / (block.timestamp - info.timestamp)) /
10000
);
}
Comment on lines +200 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting seems broken here (indentation).

if (info.ratedFlow <= limitUnlocked || timeWindow <= block.timestamp - info.timestamp) {
info.ratedFlow = 0;
} else {
info.ratedFlow -= uint224(limitUnlocked);
}
}

//increases the tracked ratedFlow for the current time window by the amount sent out
info.ratedFlow += uint224(amount);
unchecked {
//revert if the outflow exceeds rate limit
require(
info.ratedFlow <= (rateLimit * address(this).balance) / 10000
);
//sets the current time as the last transfer for the token
info.timestamp = uint32(block.timestamp);
//transfers out
payable(to).transfer(amount);
return(true);
}
//if the token is a ERC20 token
} else {
//used to get around solidity 0.8 reverts
unchecked {
if (block.timestamp != info.timestamp)
{
limitUnlocked = uint224(
(IERC20(token).balanceOf(address(this)) * rateLimit) /
(timeWindow / (block.timestamp - info.timestamp)) /
10000
);
}
if (info.ratedFlow <= limitUnlocked || timeWindow <= block.timestamp - info.timestamp) {
info.ratedFlow = 0;
} else {
info.ratedFlow -= uint224(limitUnlocked);
}
}
//increases the tracked ratedFlow for the current time window by the amount sent out
info.ratedFlow += uint224(amount);
unchecked {
//revert if the outflow exceeds rate limit
require(
info.ratedFlow <=
(rateLimit *
IERC20(token).balanceOf(address(this))) /
10000
);
//sets the current time as the last transfer for the token
info.timestamp = uint32(block.timestamp);
//transfers out
return(IERC20(token).transfer(to, amount));
}
}
}
}
}
```
### Note

The actual limit would be based on the previous 1h outflows and should that be 0 the limit approaches 2*rateLimit for that 1h slot, though to be noted does not apply for following slots so rather than 2x, net outflow limit for any time t can be seen as

rateLimit available at time 0 + timeWindow * rateLimit

Not simply timeWindow * rateLimit, where 2 * rateLimit for cases where the start outflow (at time 0 for the window) would be 0 for the previous 1h (no activity last 1h), as the outflow at time 0 increases the excess possible decreases.

So let's say that you have a timeWindow at 100m and a rateLimit at 100 bips (lets say theres 1000 tokens and so 100 tokens per 100m)
```
Time (m)
Amount out (up to limit)
000 005 010 020 040 060 080 099 100 105 110 120...
100 005 010 010 020 020 020 014 001 005 005 010...
Delta 1h
100+005+005+010+020+020+020+019+001 (200 last 1h possible at 0-100m passed)
....005+005+010+020+020+020+019+001+005 (105 last 1h possible at 05-105m passed)

Amount out last 1h window
100 105 115 125 145 165 185 199 200 105 105 110 ...
```

So realistically ends up as the excess to the rateLimit possible being the free rate available at time 0 for the window.


Making the timeWindow and rateLimit small for that window limits available rate at t0. So timeWindow at 5m, rateLimit 5 may be better than 100m and 100 bips.

Though having a a large allowance and window allow spike capacity that smoothes and makes the av rate approach target rather than a set absolute target. could be useful. At worst with a 100 bip, 1h window, would take an attacker 9h to extract vs 10 as t0 would be 1h worth excess, but they can only do up to the rateLimit every 1h after as the previous 1h would have reached the rateLimit and not be 0.


## Security Considerations
The above discussion shows why we decided to implement the per token tracking and why and how this should be used to help limit losses from hacks. Contracts must consider any whitelists very carefully as these may be able to be used to drain assets by themselves having been hacked or through non intended use. Teams and projects must also consider their usual outflows based on analytics to try and not impact user experience, balancing both the limit in potential damage from the hack as well as the user experience from transaction reverts. The getLimitLeft can be used in the front end to let users know that there is high demand and to wait for outflows from the contracts. This also does not affect approvals and transferFrom to be used on the contracts balances, so contracts must be aware and manage approvals carefully as these are not checked, using transferL where possible.

Note that `rateLimit` may be set to 1000 (100% in basis points), which effectively disables the rate limit. A flag had been considered, but it would result in higher gas fees and therefore was deemed unneccesary.

## Copyright
Copyright and related rights waived via [CC0](../LICENSE.md).