Skip to content

Commit

Permalink
TREX-129 default allowance from develop (#215)
Browse files Browse the repository at this point in the history
* ✨() TREX-129 Default allowance

* ✅(test) TREX-129 Add tests

* ✅(test) TREX-129 Add more tests

* 🐛() TREX-129 update after review

* 🐛() TREX-129 update after review 2

* 🔧(Token) improve global allowance logic

* 📝(CHANGELOG) update changelog

---------

Co-authored-by: joachimlebrun <joachim@tokeny.com>
  • Loading branch information
pgonday and Joachim-Lebrun authored Aug 16, 2024
1 parent c11603a commit 0395ed8
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 377 deletions.
5 changes: 3 additions & 2 deletions .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
"code-complexity": ["error", 7],
"function-max-lines": ["error", 50],
"max-line-length": ["error", 130],
"max-states-count": ["error", 15],
"max-states-count": ["warn", 15],
"no-empty-blocks": "error",
"no-unused-vars": "error",
"payable-fallback": "error",
"constructor-syntax": "error",
"not-rely-on-time": "off",
"reason-string": "off",
"no-global-import": "off"
"no-global-import": "off",
"gas-custom-errors": "off"
},
"plugins": ["prettier"]
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# Change Log
All notable changes to this project will be documented in this file.

## [4.2.0]

### Added

- **Default Allowance Mechanism**:
- Introduced a new feature allowing the contract owner to set certain addresses as trusted external smart contracts, enabling them to use `transferFrom` without requiring an explicit allowance from users. By default, users are opted in, allowing these contracts to have an "infinite allowance". Users can opt-out if they prefer to control allowances manually.
- Added custom errors and events to provide better feedback and traceability:
- Custom errors: `DefaultAllowanceAlreadyEnabled`, `DefaultAllowanceAlreadyDisabled`, `DefaultAllowanceAlreadySet`.
- Events: `DefaultAllowance`, `DefaultAllowanceDisabled`, `DefaultAllowanceEnabled`.
- Enhanced the `allowance` function to return `type(uint256).max` for addresses with default allowance enabled, unless the user has opted out.

## [4.1.5]

### Update
Expand Down
6 changes: 6 additions & 0 deletions contracts/errors/CommonErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,9 @@ error EnforcedPause();
* @dev The operation failed because the contract is not paused.
*/
error ExpectedPause();

/**
* @dev The operation failed because the input array is too big.
* @param _maxSize maximum size for the array.
*/
error ArraySizeLimited(uint256 _maxSize);
31 changes: 31 additions & 0 deletions contracts/token/IToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ event AgentRestrictionsSet(
bool _disablePause,
bool _disableRecovery);

/// @dev This event is emitted when the owner gives or cancels a default allowance.
/// @param _to Address of target.
/// @param _allowance Allowance or disallowance.
event DefaultAllowance(address _to, bool _allowance);

/// @dev This event is emitted when a user remove the default allowance.
/// @param _user Address of user.
event DefaultAllowanceDisabled(address _user);

/// @dev This event is emitted when a user adds the default allowance back after disabling.
/// @param _user Address of user.
event DefaultAllowanceEnabled(address _user);

/// @dev interface
interface IToken is IERC20 {
Expand Down Expand Up @@ -325,6 +337,24 @@ interface IToken is IERC20 {
address _investorOnchainID
) external returns (bool);

/**
* @dev The owner of this address can allow or disallow spending without allowance.
* Any `TransferFrom` from these targets won't need allowance (allow = true) or will need allowance (allow = false).
* @param _allow Allow or disallow spending without allowance.
* @param _targets Addresses without allowance needed.
*/
function setAllowanceForAll(bool _allow, address[] calldata _targets) external;

/**
* @dev The caller can remove default allowance globally.
*/
function disableDefaultAllowance() external;

/**
* @dev The caller can get default allowance back globally.
*/
function enableDefaultAllowance() external;

/**
* @dev function allowing to issue transfers in batch
* Require that the msg.sender and `to` addresses are not frozen.
Expand Down Expand Up @@ -518,4 +548,5 @@ interface IToken is IERC20 {
* Each flag set to `true` disables the corresponding capability for the agent.
*/
function getAgentRestrictions(address agent) external view returns (TokenRoles memory);

}
43 changes: 42 additions & 1 deletion contracts/token/Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ error TransferNotPossible();
/// @dev Thrown when identity is not verified.
error UnverifiedIdentity();

/// @dev Thrown when default allowance is already enabled for _user.
error DefaultAllowanceAlreadyEnabled(address _user);

/// @dev Thrown when default allowance is already disabled for _user.
error DefaultAllowanceAlreadyDisabled(address _user);

/// @dev Thrown when default allowance is already set for _target.
error DefaultAllowanceAlreadySet(address _target);


contract Token is IToken, AgentRoleUpgradeable, TokenStorage {

Expand Down Expand Up @@ -288,7 +297,9 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage {
uint256 balance = balanceOf(_from) - (_frozenTokens[_from]);
require(_amount <= balance, ERC20InsufficientBalance(_from, balance, _amount));
if (_tokenIdentityRegistry.isVerified(_to) && _tokenCompliance.canTransfer(_from, _to, _amount)) {
_approve(_from, msg.sender, _allowances[_from][msg.sender] - (_amount));
if (!_defaultAllowances[msg.sender] || _defaultAllowanceOptOuts[_from]) {
_approve(_from, msg.sender, _allowances[_from][msg.sender] - (_amount));
}
_transfer(_from, _to, _amount);
_tokenCompliance.transferred(_from, _to, _amount);
return true;
Expand Down Expand Up @@ -385,6 +396,31 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage {
revert RecoveryNotPossible();
}

/// @dev See {IToken-setAllowanceForAll}.
function setAllowanceForAll(bool _allow, address[] calldata _targets) external override onlyOwner {
uint256 targetsCount = _targets.length;
require(targetsCount <= 100, ArraySizeLimited(100));
for (uint256 i = 0; i < targetsCount; i++) {
require(_defaultAllowances[_targets[i]] != _allow, DefaultAllowanceAlreadySet(_targets[i]));
_defaultAllowances[_targets[i]] = _allow;
emit DefaultAllowance(_targets[i], _allow);
}
}

/// @dev See {IToken-disableDefaultAllowance}.
function disableDefaultAllowance() external override {
require(!_defaultAllowanceOptOuts[msg.sender], DefaultAllowanceAlreadyDisabled(msg.sender));
_defaultAllowanceOptOuts[msg.sender] = true;
emit DefaultAllowanceDisabled(msg.sender);
}

/// @dev See {IToken-enableDefaultAllowance}.
function enableDefaultAllowance() external override {
require(_defaultAllowanceOptOuts[msg.sender], DefaultAllowanceAlreadyEnabled(msg.sender));
_defaultAllowanceOptOuts[msg.sender] = false;
emit DefaultAllowanceEnabled(msg.sender);
}

/**
* @dev See {IERC20-totalSupply}.
*/
Expand All @@ -396,6 +432,10 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage {
* @dev See {IERC20-allowance}.
*/
function allowance(address _owner, address _spender) external view virtual override returns (uint256) {
if (_defaultAllowances[_spender] && !_defaultAllowanceOptOuts[_owner]) {
return type(uint256).max;
}

return _allowances[_owner][_spender];
}

Expand Down Expand Up @@ -676,4 +716,5 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage {
*/
// solhint-disable-next-line no-empty-blocks
function _beforeTokenTransfer(address _from, address _to, uint256 _amount) internal virtual {}

}
6 changes: 5 additions & 1 deletion contracts/token/TokenStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ contract TokenStorage {

mapping(address => TokenRoles) internal _agentsRestrictions;

mapping(address spender => bool allowance) internal _defaultAllowances;

mapping(address user => bool optOut) internal _defaultAllowanceOptOuts;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
*/
uint256[48] private __gap;
uint256[46] private __gap;
}
1 change: 1 addition & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@openzeppelin/hardhat-upgrades';
import 'solidity-coverage';
import '@nomiclabs/hardhat-solhint';
import '@primitivefi/hardhat-dodoc';
import 'hardhat-tracer';

const config: HardhatUserConfig = {
solidity: {
Expand Down
Loading

0 comments on commit 0395ed8

Please # to comment.