Skip to content
Open
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ PRIVATE_KEY=<deployer_private_key> # Deployer account (contract owner)

SILENTDATA_RPC_URL=<silentdata_rpc_url>
SILENTDATA_CHAIN_ID=<silentdata_chain_id>
CREATE2_SALT=0xab5d000000000000000000000000000000000000000000000000000000000000
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ dist
.env.test.local
.env.production.local

.vscode
.vscode

.pnpm-store/

ignition/parameters/*
!ignition/parameters/*.example
2 changes: 1 addition & 1 deletion contracts/token/UCEF.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ abstract contract UCEF is ERC20 {
* @custom:security Implementing contracts should carefully consider their authorization logic
* as it directly impacts the privacy of user balances
*/
function _authorizeBalance(address account) internal view virtual returns (bool) {}
function _authorizeBalance(address account) internal view virtual returns (bool);

/**
* @dev Returns the balance of the specified account if authorized
Expand Down
332 changes: 332 additions & 0 deletions contracts/token/XUCEF.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IXERC20} from 'xerc20/solidity/interfaces/IXERC20.sol';
import {UCEFOwned, UCEF} from '../extensions/UCEFOwned.sol';
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import {ERC20Permit} from '@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

/**
* @title XUCEF
* @notice Cross-chain extension of `UCEF` that supports rate-limited minting and burning by bridges.
* @dev Implements `IXERC20` semantics. The owner (set to the `FACTORY`) manages per-bridge
* minting and burning limits which refill linearly over `_DURATION`. A `lockbox` may
* bypass limits for custodial operations.
*/
contract XUCEF is UCEFOwned, Ownable, IXERC20, ERC20Permit {
/**
* @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;

uint8 private immutable _decimals;

/**
* @notice Constructs the initial config of the XUCEF
*
* @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,
uint8 __decimals,
address _factory
) UCEF(_name, _symbol) ERC20Permit(_name) Ownable(_factory) {
FACTORY = _factory;
_decimals = __decimals;
}

/**
* @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 minting and burning limits of a bridge
* @dev Only callable by the owner. Reverts with `IXERC20_LimitsTooHigh` if a limit exceeds `type(uint256).max / 2`.
* @param _bridge The bridge address whose limits are being set
* @param _mintingLimit The new minting max limit for the bridge
* @param _burningLimit The new burning max limit for the bridge
*/
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);
}

function balanceOf(address account) public view override(ERC20, UCEF) returns (uint256) {
/// @inheritdoc UCEF
return UCEF.balanceOf(account);
}

function totalSupply() public view override(ERC20, UCEF) returns (uint256) {
/// @inheritdoc UCEF
return UCEF.totalSupply();
}

function allowance(address owner, address spender) public view override(ERC20, UCEF) returns (uint256) {
/// @inheritdoc UCEF
return UCEF.allowance(owner, spender);
}

function decimals() public view override returns (uint8) {
return _decimals;
}

function _update(address from, address to, uint256 value) internal override(ERC20, UCEF) {
/// @inheritdoc UCEF
UCEF._update(from, to, value);
}

function _approve(address owner, address spender, uint256 value, bool emitEvent) internal override(ERC20, UCEF) {
/// @inheritdoc UCEF
UCEF._approve(owner, spender, value, emitEvent);
}

function _spendAllowance(address owner, address spender, uint256 value) internal override(ERC20, UCEF) {
/// @inheritdoc UCEF
UCEF._spendAllowance(owner, spender, value);
}
}
9 changes: 9 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { config as Config } from 'dotenv'
import '@nomicfoundation/hardhat-ignition'
import '@appliedblockchain/silentdatarollup-hardhat-plugin'
import { SignatureType } from '@appliedblockchain/silentdatarollup-core'
import './tasks/set-xerc20-limits'
import './tasks/call'

// Load .env file
Config()
Expand All @@ -18,6 +20,13 @@ const config: HardhatUserConfig = {
},
},
},
ignition: {
strategyConfig: {
create2: {
salt: process.env.CREATE2_SALT ?? '0xab5d000000000000000000000000000000000000000000000000000000000000'
}
}
},
networks: {
localhost: {
url: 'http://127.0.0.1:8545',
Expand Down
14 changes: 14 additions & 0 deletions ignition/modules/XUCEF.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'

export default buildModule('XUCEFModule', (m) => {
const name = m.getParameter('name', 'XUCEF')
const symbol = m.getParameter('symbol', 'XUCEF')
const decimals = m.getParameter('decimals', 18)
const deployer = m.getAccount(0)

const xucef = m.contract('XUCEF', [name, symbol, decimals, deployer], {
id: 'XUCEF',
})

return { xucef }
})
Loading