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

feat: permissionless token addition with GMP #261

Merged
merged 15 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14.x'
node-version: '16.x'

- run: yarn install --frozen-lockfile --non-interactive

Expand All @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14.x'
node-version: '16.x'

- run: yarn install --frozen-lockfile --non-interactive
- name: Run linter
Expand Down
12 changes: 3 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
node-version: [16.x]
steps:
- name: Checkout code
uses: actions/[email protected]
Expand Down Expand Up @@ -41,15 +41,15 @@ jobs:
run: |
fuser -k 8545/tcp
make start-forkedMainnet FORKED_TESTS_PROVIDER=${{ secrets.FORKED_TESTS_PROVIDER }}
npx truffle test testUnderForked/*
make test-forked

coverage:
needs: test
name: Coverage
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
node-version: [16.x]
steps:
- name: Checkout code
uses: actions/[email protected]
Expand All @@ -64,9 +64,3 @@ jobs:
path: build
- name: Yarn install
run: yarn install --frozen-lockfile
- name: Run coverage
run: ./node_modules/.bin/truffle run coverage -solcoverjs ./scripts/.solcover.js --network test
- name: Coverall
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ coverage/
coverage.json
src/ethers
src/web3
ganache-cli/
ganache/
dist/
data/
.vscode/
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM trufflesuite/ganache-cli
FROM trufflesuite/ganache

WORKDIR /app
COPY data/ /app/data

CMD ["ganache-cli", "--db", "data/", "-h", "0.0.0.0", "-p", "8545"]
CMD ["ganache", "--db", "data/", "-h", "0.0.0.0", "-p", "8545"]
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ install-deps:
.PHONY: test
test:
@echo " > \033[32mTesting contracts... \033[0m "
npx truffle test
truffle test --stacktrace

compile:
@echo " > \033[32mCompiling contracts... \033[0m "
npx truffle compile
truffle compile

start-ganache:
@echo " > \033[32mStarting ganache... \033[0m "
./scripts/start_ganache.sh

start-forkedMainnet:
@echo " > \033[32mStarting forked environment... \033[0m "
ganache-cli -f $(FORKED_TESTS_PROVIDER) & sleep 3
ganache -f $(FORKED_TESTS_PROVIDER) & sleep 3

test-forked:
@echo " > \033[32mTesting contracts... \033[0m "
truffle test --stacktrace testUnderForked/*

start-geth:
@echo " > \033[32mStarting geth... \033[0m "
Expand Down
286 changes: 286 additions & 0 deletions contracts/XERC20/XERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.4 <0.9.0;

import { IXERC20 } from "./interfaces/IXERC20.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/**
* @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/contracts/XERC20.sol
*/
contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit {
mpetrun5 marked this conversation as resolved.
Show resolved Hide resolved
/**
* @notice The duration it takes for the limits to fully replenish
*/
uint256 private constant _DURATION = 1 days;

/**
* @notice The address of the factory which deployed this contract
*/
address public immutable FACTORY;

/**
* @notice The address of the lockbox contract
*/
address public lockbox;

/**
* @notice Maps bridge address to bridge configurations
*/
mapping(address => Bridge) public bridges;

/**
* @notice Constructs the initial config of the XERC20
*
* @param _name The name of the token
* @param _symbol The symbol of the token
* @param _factory The factory which deployed this contract
*/
constructor(string memory _name, string memory _symbol, address _factory) ERC20(_name, _symbol) ERC20Permit(_name) {
_transferOwnership(_factory);
FACTORY = _factory;
}

/**
* @notice Mints tokens for a user
* @dev Can only be called by a bridge
* @param _user The address of the user who needs tokens minted
* @param _amount The amount of tokens being minted
*/
function mint(address _user, uint256 _amount) public {
_mintWithCaller(msg.sender, _user, _amount);
}

/**
* @notice Burns tokens for a user
* @dev Can only be called by a bridge
* @param _user The address of the user who needs tokens burned
* @param _amount The amount of tokens being burned
*/
function burn(address _user, uint256 _amount) public {
if (msg.sender != _user) {
_spendAllowance(_user, msg.sender, _amount);
}

_burnWithCaller(msg.sender, _user, _amount);
}

/**
* @notice Sets the lockbox address
*
* @param _lockbox The address of the lockbox
*/
function setLockbox(address _lockbox) public {
if (msg.sender != FACTORY) revert IXERC20_NotFactory();
lockbox = _lockbox;

emit LockboxSet(_lockbox);
}

/**
* @notice Updates the limits of any bridge
* @dev Can only be called by the owner
* @param _mintingLimit The updated minting limit we are setting to the bridge
* @param _burningLimit The updated burning limit we are setting to the bridge
* @param _bridge The address of the bridge we are setting the limits too
*/
function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external onlyOwner {
if (_mintingLimit > (type(uint256).max / 2) || _burningLimit > (type(uint256).max / 2)) {
revert IXERC20_LimitsTooHigh();
}

_changeMinterLimit(_bridge, _mintingLimit);
_changeBurnerLimit(_bridge, _burningLimit);
emit BridgeLimitsSet(_mintingLimit, _burningLimit, _bridge);
}

/**
* @notice Returns the max limit of a bridge
*
* @param _bridge the bridge we are viewing the limits of
* @return _limit The limit the bridge has
*/
function mintingMaxLimitOf(address _bridge) public view returns (uint256 _limit) {
_limit = bridges[_bridge].minterParams.maxLimit;
}

/**
* @notice Returns the max limit of a bridge
*
* @param _bridge the bridge we are viewing the limits of
* @return _limit The limit the bridge has
*/
function burningMaxLimitOf(address _bridge) public view returns (uint256 _limit) {
_limit = bridges[_bridge].burnerParams.maxLimit;
}

/**
* @notice Returns the current limit of a bridge
*
* @param _bridge the bridge we are viewing the limits of
* @return _limit The limit the bridge has
*/
function mintingCurrentLimitOf(address _bridge) public view returns (uint256 _limit) {
_limit = _getCurrentLimit(
bridges[_bridge].minterParams.currentLimit,
bridges[_bridge].minterParams.maxLimit,
bridges[_bridge].minterParams.timestamp,
bridges[_bridge].minterParams.ratePerSecond
);
}

/**
* @notice Returns the current limit of a bridge
*
* @param _bridge the bridge we are viewing the limits of
* @return _limit The limit the bridge has
*/
function burningCurrentLimitOf(address _bridge) public view returns (uint256 _limit) {
_limit = _getCurrentLimit(
bridges[_bridge].burnerParams.currentLimit,
bridges[_bridge].burnerParams.maxLimit,
bridges[_bridge].burnerParams.timestamp,
bridges[_bridge].burnerParams.ratePerSecond
);
}

/**
* @notice Uses the limit of any bridge
* @param _bridge The address of the bridge who is being changed
* @param _change The change in the limit
*/
function _useMinterLimits(address _bridge, uint256 _change) internal {
uint256 _currentLimit = mintingCurrentLimitOf(_bridge);
bridges[_bridge].minterParams.timestamp = block.timestamp;
bridges[_bridge].minterParams.currentLimit = _currentLimit - _change;
}

/**
* @notice Uses the limit of any bridge
* @param _bridge The address of the bridge who is being changed
* @param _change The change in the limit
*/
function _useBurnerLimits(address _bridge, uint256 _change) internal {
uint256 _currentLimit = burningCurrentLimitOf(_bridge);
bridges[_bridge].burnerParams.timestamp = block.timestamp;
bridges[_bridge].burnerParams.currentLimit = _currentLimit - _change;
}

/**
* @notice Updates the limit of any bridge
* @dev Can only be called by the owner
* @param _bridge The address of the bridge we are setting the limit too
* @param _limit The updated limit we are setting to the bridge
*/
function _changeMinterLimit(address _bridge, uint256 _limit) internal {
uint256 _oldLimit = bridges[_bridge].minterParams.maxLimit;
uint256 _currentLimit = mintingCurrentLimitOf(_bridge);
bridges[_bridge].minterParams.maxLimit = _limit;

bridges[_bridge].minterParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit);

bridges[_bridge].minterParams.ratePerSecond = _limit / _DURATION;
bridges[_bridge].minterParams.timestamp = block.timestamp;
}

/**
* @notice Updates the limit of any bridge
* @dev Can only be called by the owner
* @param _bridge The address of the bridge we are setting the limit too
* @param _limit The updated limit we are setting to the bridge
*/
function _changeBurnerLimit(address _bridge, uint256 _limit) internal {
uint256 _oldLimit = bridges[_bridge].burnerParams.maxLimit;
uint256 _currentLimit = burningCurrentLimitOf(_bridge);
bridges[_bridge].burnerParams.maxLimit = _limit;

bridges[_bridge].burnerParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit);

bridges[_bridge].burnerParams.ratePerSecond = _limit / _DURATION;
bridges[_bridge].burnerParams.timestamp = block.timestamp;
}

/**
* @notice Updates the current limit
*
* @param _limit The new limit
* @param _oldLimit The old limit
* @param _currentLimit The current limit
* @return _newCurrentLimit The new current limit
*/
function _calculateNewCurrentLimit(
uint256 _limit,
uint256 _oldLimit,
uint256 _currentLimit
) internal pure returns (uint256 _newCurrentLimit) {
uint256 _difference;

if (_oldLimit > _limit) {
_difference = _oldLimit - _limit;
_newCurrentLimit = _currentLimit > _difference ? _currentLimit - _difference : 0;
} else {
_difference = _limit - _oldLimit;
_newCurrentLimit = _currentLimit + _difference;
}
}

/**
* @notice Gets the current limit
*
* @param _currentLimit The current limit
* @param _maxLimit The max limit
* @param _timestamp The timestamp of the last update
* @param _ratePerSecond The rate per second
* @return _limit The current limit
*/
function _getCurrentLimit(
uint256 _currentLimit,
uint256 _maxLimit,
uint256 _timestamp,
uint256 _ratePerSecond
) internal view returns (uint256 _limit) {
_limit = _currentLimit;
if (_limit == _maxLimit) {
return _limit;
} else if (_timestamp + _DURATION <= block.timestamp) {
_limit = _maxLimit;
} else if (_timestamp + _DURATION > block.timestamp) {
uint256 _timePassed = block.timestamp - _timestamp;
uint256 _calculatedLimit = _limit + (_timePassed * _ratePerSecond);
_limit = _calculatedLimit > _maxLimit ? _maxLimit : _calculatedLimit;
}
}

/**
* @notice Internal function for burning tokens
*
* @param _caller The caller address
* @param _user The user address
* @param _amount The amount to burn
*/
function _burnWithCaller(address _caller, address _user, uint256 _amount) internal {
if (_caller != lockbox) {
uint256 _currentLimit = burningCurrentLimitOf(_caller);
if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits();
_useBurnerLimits(_caller, _amount);
}
_burn(_user, _amount);
}

/**
* @notice Internal function for minting tokens
*
* @param _caller The caller address
* @param _user The user address
* @param _amount The amount to mint
*/
function _mintWithCaller(address _caller, address _user, uint256 _amount) internal {
if (_caller != lockbox) {
uint256 _currentLimit = mintingCurrentLimitOf(_caller);
if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits();
_useMinterLimits(_caller, _amount);
}
_mint(_user, _amount);
}
}
Loading
Loading