-
-
Notifications
You must be signed in to change notification settings - Fork 89
Added ERC20 Streaming Enforcer #47
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
17094f2
Added erc20 streaming enforcer
hanzel98 63af391
Implemented linear erc20 streaming, added integration tests, changed …
hanzel98 1b81fca
Added clarification about the maxAmount usage
hanzel98 9bb76fd
Style underscore for function variable
hanzel98 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 hidden or 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
This file contains hidden or 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
This file contains hidden or 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,221 @@ | ||
| // SPDX-License-Identifier: MIT AND Apache-2.0 | ||
| pragma solidity 0.8.23; | ||
|
|
||
| import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; | ||
|
|
||
| import { CaveatEnforcer } from "./CaveatEnforcer.sol"; | ||
| import { ModeCode } from "../utils/Types.sol"; | ||
|
|
||
| /** | ||
| * @title ERC20StreamingEnforcer | ||
| * @notice This contract enforces a linear streaming transfer limit for ERC20 tokens. | ||
| * | ||
| * How it works: | ||
| * 1. Nothing is available before `startTime`. | ||
| * 2. Starting at `startTime`, `initialAmount` becomes immediately available. | ||
| * 3. Beyond that, tokens accrue linearly at `amountPerSecond`. | ||
| * 4. The total unlocked is capped by `maxAmount`. | ||
| * 5. The enforcer tracks how many tokens have already been spent, and will revert | ||
| * if an attempted transfer exceeds what remains unlocked. | ||
| * | ||
| * @dev This caveat enforcer only works when the execution is in single mode (`ModeCode.Single`). | ||
| * @dev To enable an 'infinite' token stream, set `maxAmount` to type(uint256).max | ||
| */ | ||
| contract ERC20StreamingEnforcer is CaveatEnforcer { | ||
| using ExecutionLib for bytes; | ||
|
|
||
| ////////////////////////////// State ////////////////////////////// | ||
|
|
||
| struct StreamingAllowance { | ||
| uint256 initialAmount; | ||
| uint256 maxAmount; | ||
| uint256 amountPerSecond; | ||
| uint256 startTime; | ||
| uint256 spent; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Maps a delegation manager address and delegation hash to a StreamingAllowance. | ||
| */ | ||
| mapping(address delegationManager => mapping(bytes32 delegationHash => StreamingAllowance)) public streamingAllowances; | ||
|
|
||
| ////////////////////////////// Events ////////////////////////////// | ||
|
|
||
| event IncreasedSpentMap( | ||
| address indexed sender, | ||
| address indexed redeemer, | ||
| bytes32 indexed delegationHash, | ||
| address token, | ||
| uint256 initialAmount, | ||
| uint256 maxAmount, | ||
| uint256 amountPerSecond, | ||
| uint256 startTime, | ||
| uint256 spent, | ||
| uint256 lastUpdateTimestamp | ||
| ); | ||
|
|
||
| ////////////////////////////// Public Methods ////////////////////////////// | ||
|
|
||
| /** | ||
| * @notice Retrieves the current available allowance for a specific delegation. | ||
| * @param _delegationManager The address of the delegation manager. | ||
| * @param _delegationHash The hash of the delegation being queried. | ||
| * @return availableAmount_ The number of tokens that are currently spendable | ||
| * under this streaming allowance (capped by `maxAmount`). | ||
| */ | ||
| function getAvailableAmount( | ||
| address _delegationManager, | ||
| bytes32 _delegationHash | ||
| ) | ||
| external | ||
| view | ||
| returns (uint256 availableAmount_) | ||
| { | ||
| StreamingAllowance storage allowance_ = streamingAllowances[_delegationManager][_delegationHash]; | ||
| availableAmount_ = _getAvailableAmount(allowance_); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Hook called before an ERC20 transfer is executed to enforce streaming limits. | ||
| * @dev This function will revert if the transfer amount exceeds the available streaming allowance. | ||
| * @param _terms 148 packed bytes where: | ||
| * - 20 bytes: ERC20 token address. | ||
| * - 32 bytes: initial amount. | ||
| * - 32 bytes: max amount. | ||
| * - 32 bytes: amount per second. | ||
| * - 32 bytes: start time for the streaming allowance. | ||
| * @param _mode The mode of the execution (must be `ModeCode.Single` for this enforcer). | ||
| * @param _executionCallData The transaction the delegate might try to perform. | ||
| * @param _delegationHash The hash of the delegation being operated on. | ||
| * @param _redeemer The address of the redeemer. | ||
| */ | ||
| function beforeHook( | ||
| bytes calldata _terms, | ||
| bytes calldata, | ||
| ModeCode _mode, | ||
| bytes calldata _executionCallData, | ||
| bytes32 _delegationHash, | ||
| address, | ||
| address _redeemer | ||
| ) | ||
| public | ||
| override | ||
| onlySingleExecutionMode(_mode) | ||
| { | ||
| _validateAndConsumeAllowance(_terms, _executionCallData, _delegationHash, _redeemer); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Decodes the terms used in this CaveatEnforcer. | ||
| * @param _terms 148 packed bytes where: | ||
| * - 20 bytes: ERC20 token address. | ||
| * - 32 bytes: initial amount. | ||
| * - 32 bytes: max amount. | ||
| * - 32 bytes: amount per second. | ||
| * - 32 bytes: start time for the streaming allowance. | ||
| * @return token_ The address of the ERC20 token contract. | ||
| * @return initialAmount_ The initial amount available at startTime. | ||
| * @return maxAmount_ The maximum total unlocked tokens (hard cap) | ||
| * @return amountPerSecond_ The rate at which the allowance increases per second. | ||
| * @return startTime_ The timestamp from which the allowance streaming begins. | ||
| */ | ||
| function getTermsInfo(bytes calldata _terms) | ||
| public | ||
| pure | ||
| returns (address token_, uint256 initialAmount_, uint256 maxAmount_, uint256 amountPerSecond_, uint256 startTime_) | ||
| { | ||
| require(_terms.length == 148, "ERC20StreamingEnforcer:invalid-terms-length"); | ||
|
|
||
| token_ = address(bytes20(_terms[0:20])); | ||
| initialAmount_ = uint256(bytes32(_terms[20:52])); | ||
| maxAmount_ = uint256(bytes32(_terms[52:84])); | ||
| amountPerSecond_ = uint256(bytes32(_terms[84:116])); | ||
| startTime_ = uint256(bytes32(_terms[116:148])); | ||
| } | ||
|
|
||
| ////////////////////////////// Internal Methods ////////////////////////////// | ||
|
|
||
| /** | ||
| * @notice Validates the streaming allowance limit and updates `spent`. | ||
| * @dev Reverts if the transfer amount exceeds the currently available allowance. | ||
| * | ||
| * @param _terms The encoded streaming terms: ERC20 token, initial amount, max amount, amount per second, and start time. | ||
| * @param _executionCallData The transaction data specifying the target contract and call data. Expect | ||
| * an `IERC20.transfer(address,uint256)` call here. | ||
| * @param _delegationHash The hash of the delegation to which this transfer applies. | ||
| * @param _redeemer The address of the redeemer. | ||
| */ | ||
| function _validateAndConsumeAllowance( | ||
| bytes calldata _terms, | ||
| bytes calldata _executionCallData, | ||
| bytes32 _delegationHash, | ||
| address _redeemer | ||
| ) | ||
| private | ||
| { | ||
| (address target_,, bytes calldata callData_) = _executionCallData.decodeSingle(); | ||
|
|
||
| require(callData_.length == 68, "ERC20StreamingEnforcer:invalid-execution-length"); | ||
|
|
||
| (address token_, uint256 initialAmount_, uint256 maxAmount_, uint256 amountPerSecond_, uint256 startTime_) = | ||
| getTermsInfo(_terms); | ||
|
|
||
| require(maxAmount_ >= initialAmount_, "ERC20StreamingEnforcer:invalid-max-amount"); | ||
|
|
||
| require(startTime_ > 0, "ERC20StreamingEnforcer:invalid-zero-start-time"); | ||
|
|
||
| require(token_ == target_, "ERC20StreamingEnforcer:invalid-contract"); | ||
|
|
||
| require(bytes4(callData_[0:4]) == IERC20.transfer.selector, "ERC20StreamingEnforcer:invalid-method"); | ||
|
|
||
| StreamingAllowance storage allowance_ = streamingAllowances[msg.sender][_delegationHash]; | ||
| if (allowance_.spent == 0) { | ||
| // First use of this delegation | ||
| allowance_.initialAmount = initialAmount_; | ||
| allowance_.maxAmount = maxAmount_; | ||
| allowance_.amountPerSecond = amountPerSecond_; | ||
| allowance_.startTime = startTime_; | ||
| } | ||
|
|
||
| uint256 transferAmount_ = uint256(bytes32(callData_[36:68])); | ||
|
|
||
| require(transferAmount_ <= _getAvailableAmount(allowance_), "ERC20StreamingEnforcer:allowance-exceeded"); | ||
|
|
||
| allowance_.spent += transferAmount_; | ||
|
|
||
| emit IncreasedSpentMap( | ||
| msg.sender, | ||
| _redeemer, | ||
| _delegationHash, | ||
| token_, | ||
| initialAmount_, | ||
| maxAmount_, | ||
| amountPerSecond_, | ||
| startTime_, | ||
| allowance_.spent, | ||
| block.timestamp | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Calculates how many tokens are currently unlocked in total, then subtracts `spent`, then clamps by `maxAmount`. | ||
| * @param _allowance The StreamingAllowance struct containing allowance details. | ||
| * @return A uint256 representing how many tokens are currently available to spend. | ||
| */ | ||
| function _getAvailableAmount(StreamingAllowance memory _allowance) private view returns (uint256) { | ||
| if (block.timestamp < _allowance.startTime) return 0; | ||
|
|
||
| uint256 elapsed_ = block.timestamp - _allowance.startTime; | ||
|
|
||
| uint256 unlocked_ = _allowance.initialAmount + (_allowance.amountPerSecond * elapsed_); | ||
|
|
||
| if (unlocked_ > _allowance.maxAmount) { | ||
| unlocked_ = _allowance.maxAmount; | ||
| } | ||
|
|
||
| if (_allowance.spent >= unlocked_) return 0; | ||
|
|
||
| return unlocked_ - _allowance.spent; | ||
| } | ||
| } | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.