Skip to content

Commit 4ff12b0

Browse files
cairoethernestognw
andauthored
Add ERC20 stablecoin extensions (#14)
* Add `ERC20Blocklist` base * Add `ERC20Allowlist` base * Add `ERC20Blocklist` base * Add allowlist and blocklist tests * Add custodian tests * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Update allowlist based on feedback * Update other extensions and tests * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * Update block/unblock functions * Combine freeze functions --------- Co-authored-by: Ernesto García <[email protected]>
1 parent 444989f commit 4ff12b0

File tree

8 files changed

+737
-0
lines changed

8 files changed

+737
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20, ERC20Custodian} from "../../token/ERC20/extensions/ERC20Custodian.sol";
6+
7+
abstract contract ERC20CustodianMock is ERC20Custodian {
8+
address private immutable _custodian;
9+
10+
constructor(address custodian, string memory name_, string memory symbol_) ERC20(name_, symbol_) {
11+
_custodian = custodian;
12+
}
13+
14+
function _isCustodian(address user) internal view override returns (bool) {
15+
return user == _custodian;
16+
}
17+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6+
7+
/**
8+
* @dev Extension of {ERC20} that allows to implement an allowlist
9+
* mechanism that can be managed by an authorized account with the
10+
* {_disallowUser} and {_allowUser} functions.
11+
*
12+
* The allowlist provides the guarantee to the contract owner
13+
* (e.g. a DAO or a well-configured multisig) that any account won't be
14+
* able to execute transfers or approvals to other entities to operate
15+
* on its behalf if {_allowUser} was not called with such account as an
16+
* argument. Similarly, the account will be disallowed again if
17+
* {_disallowUser} is called.
18+
*/
19+
abstract contract ERC20Allowlist is ERC20 {
20+
/**
21+
* @dev Allowed status of addresses. True if allowed, False otherwise.
22+
*/
23+
mapping(address account => bool) internal _allowed;
24+
25+
/**
26+
* @dev Emitted when a `user` is allowed to transfer and approve.
27+
*/
28+
event UserAllowed(address indexed user);
29+
30+
/**
31+
* @dev Emitted when a user is disallowed.
32+
*/
33+
event UserDisallowed(address indexed user);
34+
35+
/**
36+
* @dev The operation failed because the user is not allowed.
37+
*/
38+
error ERC20Disallowed(address user);
39+
40+
/**
41+
* @dev Returns the allowed status of an account.
42+
*/
43+
function allowed(address account) public virtual returns (bool) {
44+
return _allowed[account];
45+
}
46+
47+
/**
48+
* @dev Allows a user to receive and transfer tokens, including minting and burning.
49+
*/
50+
function _allowUser(address user) internal virtual returns (bool) {
51+
bool isAllowed = allowed(user);
52+
if (!isAllowed) {
53+
_allowed[user] = true;
54+
emit UserAllowed(user);
55+
}
56+
return isAllowed;
57+
}
58+
59+
/**
60+
* @dev Disallows a user from receiving and transferring tokens, including minting and burning.
61+
*/
62+
function _disallowUser(address user) internal virtual returns (bool) {
63+
bool isAllowed = allowed(user);
64+
if (isAllowed) {
65+
_allowed[user] = false;
66+
emit UserDisallowed(user);
67+
}
68+
return isAllowed;
69+
}
70+
71+
/**
72+
* @dev See {ERC20-_update}.
73+
*/
74+
function _update(address from, address to, uint256 value) internal virtual override {
75+
if (from != address(0) && !allowed(from)) revert ERC20Disallowed(from);
76+
if (to != address(0) && !allowed(to)) revert ERC20Disallowed(to);
77+
super._update(from, to, value);
78+
}
79+
80+
/**
81+
* @dev See {ERC20-_approve}.
82+
*/
83+
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual override {
84+
if (!allowed(owner)) revert ERC20Disallowed(owner);
85+
super._approve(owner, spender, value, emitEvent);
86+
}
87+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6+
7+
/**
8+
* @dev Extension of {ERC20} that allows to implement a blocklist
9+
* mechanism that can be managed by an authorized account with the
10+
* {_blockUser} and {_unblockUser} functions.
11+
*
12+
* The blocklist provides the guarantee to the contract owner
13+
* (e.g. a DAO or a well-configured multisig) that any account won't be
14+
* able to execute transfers or approvals to other entities to operate
15+
* on its behalf if {_blockUser} was not called with such account as an
16+
* argument. Similarly, the account will be unblocked again if
17+
* {_unblockUser} is called.
18+
*/
19+
abstract contract ERC20Blocklist is ERC20 {
20+
/**
21+
* @dev Blocked status of addresses. True if blocked, False otherwise.
22+
*/
23+
mapping(address user => bool) internal _blocked;
24+
25+
/**
26+
* @dev Emitted when a user is blocked.
27+
*/
28+
event UserBlocked(address indexed user);
29+
30+
/**
31+
* @dev Emitted when a user is unblocked.
32+
*/
33+
event UserUnblocked(address indexed user);
34+
35+
/**
36+
* @dev The operation failed because the user is blocked.
37+
*/
38+
error ERC20Blocked(address user);
39+
40+
/**
41+
* @dev Returns the blocked status of an account.
42+
*/
43+
function blocked(address account) public virtual returns (bool) {
44+
return _blocked[account];
45+
}
46+
47+
/**
48+
* @dev Blocks a user from receiving and transferring tokens, including minting and burning.
49+
*/
50+
function _blockUser(address user) internal virtual returns (bool) {
51+
bool isBlocked = blocked(user);
52+
if (!isBlocked) {
53+
_blocked[user] = true;
54+
emit UserBlocked(user);
55+
}
56+
return isBlocked;
57+
}
58+
59+
/**
60+
* @dev Unblocks a user from receiving and transferring tokens, including minting and burning.
61+
*/
62+
function _unblockUser(address user) internal virtual returns (bool) {
63+
bool isBlocked = blocked(user);
64+
if (isBlocked) {
65+
_blocked[user] = false;
66+
emit UserUnblocked(user);
67+
}
68+
return isBlocked;
69+
}
70+
71+
/**
72+
* @dev See {ERC20-_update}.
73+
*/
74+
function _update(address from, address to, uint256 value) internal virtual override {
75+
if (blocked(from)) revert ERC20Blocked(from);
76+
if (blocked(to)) revert ERC20Blocked(to);
77+
super._update(from, to, value);
78+
}
79+
80+
/**
81+
* @dev See {ERC20-_approve}.
82+
*/
83+
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual override {
84+
if (blocked(owner)) revert ERC20Blocked(owner);
85+
super._approve(owner, spender, value, emitEvent);
86+
}
87+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6+
7+
/**
8+
* @dev Extension of {ERC20} that allows to implement a custodian
9+
* mechanism that can be managed by an authorized account with the
10+
* {freeze} and {unfreeze} functions.
11+
*
12+
* This mechanism allows a custodian (e.g. a DAO or a
13+
* well-configured multisig) to freeze and unfreeze the balance
14+
* of a user.
15+
*
16+
* The frozen balance is not available for transfers or approvals
17+
* to other entities to operate on its behalf if {freeze} was not
18+
* called with such account as an argument. Similarly, the account
19+
* will be unfrozen again if {unfreeze} is called.
20+
*/
21+
abstract contract ERC20Custodian is ERC20 {
22+
/**
23+
* @dev The amount of tokens frozen by user address.
24+
*/
25+
mapping(address user => uint256 amount) internal _frozen;
26+
27+
/**
28+
* @dev Emitted when tokens are frozen for a user.
29+
* @param user The address of the user whose tokens were frozen.
30+
* @param amount The amount of tokens that were frozen.
31+
*/
32+
event TokensFrozen(address indexed user, uint256 amount);
33+
34+
/**
35+
* @dev Emitted when tokens are unfrozen for a user.
36+
* @param user The address of the user whose tokens were unfrozen.
37+
* @param amount The amount of tokens that were unfrozen.
38+
*/
39+
event TokensUnfrozen(address indexed user, uint256 amount);
40+
41+
/**
42+
* @dev The operation failed because the user has insufficient unfrozen balance.
43+
*/
44+
error ERC20InsufficientUnfrozenBalance(address user);
45+
46+
/**
47+
* @dev The operation failed because the user has insufficient frozen balance.
48+
*/
49+
error ERC20InsufficientFrozenBalance(address user);
50+
51+
/**
52+
* @dev Error thrown when a non-custodian account attempts to perform a custodian-only operation.
53+
*/
54+
error ERC20NotCustodian();
55+
56+
/**
57+
* @dev Modifier to restrict access to custodian accounts only.
58+
*/
59+
modifier onlyCustodian() {
60+
if (!_isCustodian(_msgSender())) revert ERC20NotCustodian();
61+
_;
62+
}
63+
64+
/**
65+
* @dev Returns the amount of tokens frozen for a user.
66+
*/
67+
function frozen(address user) public view virtual returns (uint256) {
68+
return _frozen[user];
69+
}
70+
71+
/**
72+
* @dev Adjusts the amount of tokens frozen for a user.
73+
* @param user The address of the user whose tokens to freeze.
74+
* @param amount The amount of tokens frozen.
75+
*
76+
* Requirements:
77+
*
78+
* - The user must have sufficient unfrozen balance.
79+
*/
80+
function freeze(address user, uint256 amount) external virtual onlyCustodian {
81+
if (availableBalance(user) < amount) revert ERC20InsufficientUnfrozenBalance(user);
82+
_frozen[user] = amount;
83+
emit TokensFrozen(user, amount);
84+
}
85+
86+
/**
87+
* @dev Returns the available (unfrozen) balance of an account.
88+
* @param account The address to query the available balance of.
89+
* @return available The amount of tokens available for transfer.
90+
*/
91+
function availableBalance(address account) public view returns (uint256 available) {
92+
available = balanceOf(account) - frozen(account);
93+
}
94+
95+
/**
96+
* @dev Checks if the user is a custodian.
97+
* @param user The address of the user to check.
98+
* @return True if the user is authorized, false otherwise.
99+
*/
100+
function _isCustodian(address user) internal view virtual returns (bool);
101+
102+
function _update(address from, address to, uint256 value) internal virtual override {
103+
if (from != address(0) && availableBalance(from) < value) revert ERC20InsufficientUnfrozenBalance(from);
104+
super._update(from, to, value);
105+
}
106+
}

0 commit comments

Comments
 (0)