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 ERC: Exponential Curves #498

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
118 changes: 118 additions & 0 deletions ERCS/erc-TBA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Exponential Curves
description: A dynamic model for exponential curves to handle various time-based events
author: Guilherme Neves (@0xneves)
discussions-to: https://ethereum-magicians.org/t/erc-xxxx-exponential-curves/20170
status: Draft
type: Standards Track
category: ERC
created: 2024-06-19
---

## Abstract

This proposal suggests an exponential curve formula designed to handle various time-based events such as token vesting, game mechanics, unlock schedules, and other timestamp-dependent actions. The core functionality is driven by an exponential curve formula allowing for smooth, nonlinear transitions over time, providing a more sophisticated and flexible approach than linear models.

## Motivation

Inspired by ENS's premium decay curve, which reduces the cost of premium names over time, this proposal aims to create a more general-purpose curve that can be used in various applications. Since calculating exponentials in Solidity is not easy because of its fixed-point arithmetic, developers are obliged to use simpler equations for growth or decay, and most times linear. Providing more nuanced control over how parameters evolve over time, leading to more sophisticated applications and user experiences.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

The interface is defined as follows:

```solidity
interface IEXPCurves {
/**
* @notice This function calculates the exponential decay value over time.
* @param currentTimeframe The current timestamp or a point within the spectrum
* @param initialTimeframe The initial timestamp or the beginning of the curve
* @param finalTimeframe The final timestamp or the end of the curve
* @param curvature The curvature factor. Determines the steepness of the curve and can be
* negative, which will invert the curve's direction.
* @param ascending The curve direction (ascending or descending)
* @return int256 The exponential decay value at a specific interval
*/
function expcurve(
uint32 currentTimeframe,
uint32 initialTimeframe,
uint32 finalTimeframe,
int16 curvature,
bool ascending
) external pure returns (int256);
}
```

The `expcurve` method calculates the curve's decay value at a *given timestamp* based on the *initial timestamp*, *final timestamp*, *curvature*, and *curve direction* (ascending or descending). The function returns the curve value as a percentage (0-100) in the form of a fixed-point number with 18 decimal places.

Both curves are controlled by the following formulas:

#### Ascending Curve

$$\frac{\exp(k \cdot \frac{t - t_0}{T - t_0}) - 1}{\exp(k) - 1} \cdot 100$$

#### Descending Curve

$$\frac{\exp(k \cdot (1 - \frac{t - t_0}{T - t_0})) - 1}{\exp(k) - 1} \cdot 100$$

Where:

- `t` is the current timestamp.
- `t0` is the start timestamp.
- `T` is the end timestamp.
- `k` is the curvature factor, determining the steepness of the curve (2 decimals precision).
- `exp()` is the exponential function with base 'E' (Euler's number, approximately 2.71828).

The *ascending curve* starts at 0% and increases to 100% over time, while the *descending curve* starts at 100% and decreases to 0% over time.

The *curvature* factor *k* allows for fine-tuning the curve's shape, providing a wide range of possibilities for customizing the curve's behavior. A higher *k* value results in a steeper curve, while a lower *k* value results in a flatter curve. This flexibility enables developers to create complex time-based scenarios with precise control over the curve's progression. For better precision, the curvature factor is an integer with two (2) decimal places, allowing for a range of -100.00 to 100.00.

- The `initialTimeframe` **MUST** be less than or equal to the `currentTimeframe`.
- The `initialTimeframe` **MUST** be less than the `finalTimeframe`.
- The `curvature` **MUST NOT** be zero.
- The `curvature` **MAY** fit between `-100.00` and `100.00` (-10_000 ~ 10_000 int16 with 2 decimals precision).
- The curve direction when `true` is ascending and when `false` is descending.

## Rationale

EXPCurves was inspired by Valocracy and ENS. Both projects have a usage for the exponential curve. Valocracy uses the curve to decrease soul-bounded governance power over time and ENS uses the curve to decrease the price of premium for an expired domain over time.

This formula was elaborated in a way that would facilitate developer experience when using exponential curves. The formula is simple and easy to understand, and the parameters are intuitive. The curve's behavior can be easily adjusted by changing the curvature factor, allowing developers to create a wide range of time-based scenarios.

The formula is not easy to implement in Solidity because relies on fixed-point arithmetic and there is nowhere to find a complete, dynamic, and easy-to-use, implementation of it. This design aims to fill this gap and provide scalar curves so that all projects can harness their power and manage time-based events in a more sophisticated way.

ERC-165 can optionally be implemented if you want integrations to detect the EXPCurves interface implementation.

## Backwards Compatibility

No backward compatibility issues were found.

## Test Cases

Tests are included in [`EXPCurves.test.ts`](../assets/eip-TBA/test/EXPCurves.test.ts).

To run them, open the terminal and use the following commands:

```
cd ./assets/erc-TBA
npm install
npm test
```

## Reference Implementation

See [`EXPCurves.sol`](../assets/eip-TBA/contracts/EXPCurves.sol).

## Security Considerations

The implementation of the curve **SHOULD** be carefully reviewed to avoid potential vulnerabilities and edge cases regarding overflows and underflows since Solidity cannot handle fixed-point arithmetic. The `expcurve` function **SHOULD** be thoroughly tested to ensure that the curve's parameters are correctly set and that it behaves as expected.

The output of the curve has 18 decimals and **MUST** be normalized to handle the result where it will be used to avoid precision issues.

When using the curve in a smart contract, developers **SHOULD** be aware of the potential gas costs associated with the calculations varying between 15.000 ~ 20.000 gas per call.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
82 changes: 82 additions & 0 deletions assets/erc-TBA/contracts/EXPCurves.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IEXPCurves} from "./IEXPCurves.sol";
import {exp} from "@prb/math/src/sd59x18/Math.sol";
import {wrap, unwrap} from "@prb/math/src/sd59x18/Casting.sol";

/**
* @title Exponential Curves
* @author https://github.com/0xneves
* @notice This smart contract implements an advanced exponential curve formula designed to
* handle various time-based events such as token vesting, game mechanics, unlock schedules,
* and other timestamp-dependent actions. The core functionality is driven by an exponential
* curve formula that allows for smooth, nonlinear transitions over time, providing a more
* sophisticated and flexible approach compared to linear models.
*/
abstract contract EXPCurves is IEXPCurves {
/**
* @notice The initial timeframe is invalid.
*
* Requirements:
*
* - Must be less than or equal to the current timestamp
* - Must be less than the final timestamp.
*/
error EXPCurveInvalidInitialTimeframe();

/**
* @notice The curvature factor is invalid.
*
* Requirements:
*
* - It cannot be zero
* - The curvature cannot be bigger than 10000 or smaller than -10000 (2 decimals precision)
*
* NOTE: Cannot be bigger than type uint of value 133 while using regular unix timestamps.
* For negative values it can go way further than type int of value -133, but there is no
* need to go that far.
*/
error EXPCurveInvalidCurvature();

/**
* @dev See {IEXPCurves-expcurve}.
*/
function expcurve(
uint32 currentTimeframe,
uint32 initialTimeframe,
uint32 finalTimeframe,
int16 curvature,
bool ascending
) public pure virtual returns (int256) {
if (initialTimeframe > currentTimeframe)
revert EXPCurveInvalidInitialTimeframe();
if (initialTimeframe >= finalTimeframe)
revert EXPCurveInvalidInitialTimeframe();
if (curvature == 0 || curvature > 10_000 || curvature < -10_000)
revert EXPCurveInvalidCurvature();
if (currentTimeframe > finalTimeframe) {
return ascending ? int(100 * 1e18) : int(0);
}
// Calculate the Time Delta and Total Time Interval
int256 td = int(uint256(currentTimeframe - initialTimeframe));
int256 tti = int(uint256(finalTimeframe - initialTimeframe));

// Calculate the Time Elapsed Ratio
int256 ter = unwrap(wrap(td) / wrap(tti));
int256 cs; // Curve Scaling
if (ascending) {
cs = (curvature * int(ter)) / 100;
} else {
cs = (curvature * (1e18 - int(ter))) / 100;
}

// Calculate the Exponential Decay
int256 expo = unwrap(exp(wrap(cs))) - 1e18;
// Calculate the Final Exponential Scaling
int256 fes = unwrap(exp(wrap(int(curvature) * 1e16))) - 1e18;

// Normalize the Exponential Decay
return unwrap(wrap(expo) / wrap(fes)) * 100;
}
}
51 changes: 51 additions & 0 deletions assets/erc-TBA/contracts/IEXPCurves.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IEXPCurves {
/**
* @dev This function calculates the exponential decay value over time.
*
* This formula ensures that the value starts at 100%/0% at the beginning (t0)
* and decreases/increases to 0%/100% at the end (T), following an exponential decay curve.
*
* The formula used for the curves difers based on the `ascending` parameter:
*
* ascending = ((exp(k * (1 - (t - t0) / (T - t0))) - 1) / (exp(k) - 1)) * 100
* descenging = ((exp(k * ((t - t0) / (T - t0))) - 1) / (exp(k) - 1)) * 100
*
* Where:
* - t is the current timestamp
* - t0 is the start timestamp
* - T is the end timestamp
* - k is the curvature factor, determining the steepness of the curve (2 decimals precision)
* - exp() is the exponential function with base 'E' (Euler's number, approximately 2.71828)
*
* Requirements:
*
* - The initial timestamp must be less than or equal to the current timestamp
* - The initial timestamp must be less than the final timestamp
* - The curvature cannot be zero
* - The curvature cannot be bigger than 10000 or smaller than -10000 (2 decimals precision)
*
* NOTE: To avoid precision issues, the formula uses fixed-point math with 18 decimals.
* When returning this function result, make sure to adjust the output values accordingly.
*
* NOTE: Using type uint32 for timestamps since 4294967295 unix seconds will only overflow
* in the year 2106, which is more than enough for the current use cases.
*
* @param currentTimeframe The current timestamp or a point within the spectrum
* @param initialTimeframe The initial timestamp or the beginning of the curve
* @param finalTimeframe The final timestamp or the end of the curve
* @param curvature The curvature factor. Determines the steepness of the curve and can be
* negative, which will invert the curve's direction.
* @param ascending The curve direction (ascending or descending)
* @return int256 The exponential decay value at a specific interval
*/
function expcurve(
uint32 currentTimeframe,
uint32 initialTimeframe,
uint32 finalTimeframe,
int16 curvature,
bool ascending
) external pure returns (int256);
}
54 changes: 54 additions & 0 deletions assets/erc-TBA/contracts/Valocracy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./EXPCurves.sol";

/**
* @title Valocracy
* @dev This contract implements a voting power system that decays over time
* using EXP curves. The balanceOf function resembles a token balance in
* ERC20Votes for Governance usage, but it decays over time based on the user's
* voting power and the curvature factor.
*/
contract Valocracy is EXPCurves {
struct User {
uint256 votingPower;
uint32 lastUpdate;
}

bool public ascending = false;
int8 public curvature;
uint32 public vacationPeriod;

mapping(address => User) public votingPower;

function balanceOf(
address account
) public view returns (int256 _adjustedPower) {
uint32 _lastUpdate = votingPower[account].lastUpdate;
uint256 _votingPower = votingPower[account].votingPower;

int256 decay = expcurve(
uint32(block.timestamp),
_lastUpdate,
_lastUpdate + vacationPeriod,
curvature,
ascending
);

_adjustedPower = (int(_votingPower) * decay) / 100 / 1e18;
}

function contribute(address _account, uint256 _votingPower) public {
votingPower[_account].votingPower = _votingPower;
votingPower[_account].lastUpdate = uint32(block.timestamp);
}

function setCurvature(int8 _curvature) public {
curvature = _curvature;
}

function setVacationPeriod(uint32 _vacationPeriod) public {
vacationPeriod = _vacationPeriod;
}
}
8 changes: 8 additions & 0 deletions assets/erc-TBA/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
solidity: "0.8.20",
};

export default config;
26 changes: 26 additions & 0 deletions assets/erc-TBA/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "expcurves",
"scripts": {
"test": "npx hardhat test"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.8",
"@nomicfoundation/hardhat-toolbox": "^2.0.1",
"@nomiclabs/hardhat-ethers": "^2.2.2",
"@nomiclabs/hardhat-etherscan": "^3.1.5",
"@nomiclabs/hardhat-solhint": "^3.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@prb/math": "^4.0.2",
"@typechain/ethers-v5": "^10.2.0",
"@typechain/hardhat": "^6.1.5",
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"chai": "^4.3.4",
"ethers": "^5.6.1",
"hardhat": "^2.12.7",
"ts-node": "^10.9.1",
"typechain": "^8.1.1",
"typescript": "^4.9.5"
}
}
Loading
Loading