-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
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 4e0deaa
Update eip-transfer_rate_limit
pr0toshi 7e1f0ea
Update EIPS/eip-transfer_rate_limit
pr0toshi 829dfe8
Update eip-transfer_rate_limit
pr0toshi 8bcb271
Update EIPS/eip-transfer_rate_limit
pr0toshi a52ce67
Rename EIPS/eip-transfer_rate_limit to EIPS/EIPs/eip-5075.md
pr0toshi e4ed10d
Update eip-5075.md
pr0toshi 923e9cb
Update eip-5075.md
pr0toshi 64b54b3
Create eip-5075.md
pr0toshi d659953
Update eip-5075.md
pr0toshi 629d181
Update eip-5075.md
pr0toshi a161734
Update eip-5075.md
pr0toshi 9528bcf
Update eip-5075.md
richance 3e0f8ca
Merge pull request #1 from richance/patch-2
pr0toshi a2aa3ed
Update eip-5075.md
pr0toshi 16b66dc
Update eip-5075.md
pr0toshi 731f021
Update eip-5075.md
pr0toshi 630a1aa
Update eip-5075.md
pr0toshi 1711b11
Update eip-5075.md
pr0toshi ff82a98
Update eip-5075.md
pr0toshi 7a78f20
Update eip-5075.md
pr0toshi a3a1878
Update EIPS/EIPs/eip-5075.md
pr0toshi 4b5a4db
Update eip-5075.md
pr0toshi 37b077c
Create eip-5075.md
pr0toshi 188f7cc
Update EIPS/EIPs/eip-5075.md
pr0toshi 6f0ac6d
Update EIPS/EIPs/eip-5075.md
pr0toshi fa8d766
Update eip-5075.md
pr0toshi 5b9bf4a
Update eip-5075.md
pr0toshi 158f0c0
Update EIPS/EIPs/eip-5075.md
pr0toshi 7f923fa
Update eip-5075.md
pr0toshi 3d91b0d
fix issue with semicolon
lightclient ac23314
Update EIPS/EIPs/eip-5075.md
pr0toshi 987bdf3
Update EIPS/EIPs/eip-5075.md
pr0toshi 5d1a8ea
Update EIPS/EIPs/eip-5075.md
pr0toshi 0dce148
Update EIPS/EIPs/eip-5075.md
pr0toshi 9f06d0d
Update EIPS/EIPs/eip-5075.md
pr0toshi 6f2d816
Update EIPS/EIPs/eip-5075.md
pr0toshi c35a3a9
Update EIPS/EIPs/eip-5075.md
pr0toshi 76d8e68
Update EIPS/EIPs/eip-5075.md
pr0toshi c46e490
Update EIPS/EIPs/eip-5075.md
pr0toshi 7c7fec6
Update EIPS/EIPs/eip-5075.md
pr0toshi 16cbbfb
Rename EIPS/EIPs/eip-5075.md to EIPS/eip-5075.md
MicahZoltu bc0a53a
No colons in title
Pandapip1 d396ac9
Merge branch 'master' into patch-1
Pandapip1 fcb882e
Apply suggestions from code review
Pandapip1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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 | ||
--- | ||
|
||
## 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a dependency on ERC-20 from this EIP?