Skip to content

Commit 423f5e2

Browse files
committed
feat: stableTokenV3 crosschain token
1 parent 5217023 commit 423f5e2

6 files changed

Lines changed: 319 additions & 25 deletions

File tree

contracts/interfaces/IStableTokenV3.sol

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
pragma solidity 0.8.24;
33

4+
/**
5+
* @title IStableTokenV3
6+
* @notice Interface for the StableTokenV3 contract.
7+
*/
48
interface IStableTokenV3 {
59
/**
610
* @notice Initializes a StableTokenV3.
@@ -164,15 +168,6 @@ interface IStableTokenV3 {
164168
bytes32 s
165169
) external;
166170

167-
/**
168-
* @notice Transfer token for a specified address
169-
* @param to The address to transfer to.
170-
* @param value The amount to be transferred.
171-
* @param comment The transfer comment.
172-
* @return True if the transaction succeeds.
173-
*/
174-
function transferWithComment(address to, uint256 value, string calldata comment) external returns (bool);
175-
176171
/**
177172
* @notice Transfer token from a specified address to the stability pool.
178173
* @param _sender The address to transfer from.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity 0.8.24;
3+
import {
4+
IERC20PermitUpgradeable
5+
} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/draft-IERC20PermitUpgradeable.sol";
6+
7+
interface IStableTokenV3CrossChain is IERC20PermitUpgradeable {
8+
/**
9+
* @notice Initializes a StableTokenV3.
10+
* @param _name The name of the stable token (English)
11+
* @param _symbol A short symbol identifying the token (e.g. "cUSD")
12+
* @param initialBalanceAddresses Array of addresses with an initial balance.
13+
* @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses.
14+
* @param _minters The addresses that are allowed to mint.
15+
* @param _burners The addresses that are allowed to burn.
16+
*/
17+
function initialize(
18+
string calldata _name,
19+
string calldata _symbol,
20+
address[] calldata initialBalanceAddresses,
21+
uint256[] calldata initialBalanceValues,
22+
address[] calldata _minters,
23+
address[] calldata _burners
24+
) external;
25+
/**
26+
* @notice Sets the minter role for an address.
27+
* @param _minter The address of the minter.
28+
* @param _isMinter The boolean value indicating if the address is a minter.
29+
*/
30+
function setMinter(address _minter, bool _isMinter) external;
31+
32+
/**
33+
* @notice Sets the burner role for an address.
34+
* @param _burner The address of the burner.
35+
* @param _isBurner The boolean value indicating if the address is a burner.
36+
*/
37+
function setBurner(address _burner, bool _isBurner) external;
38+
39+
/**
40+
* @notice Mints new StableToken and gives it to 'to'.
41+
* @param to The account for which to mint tokens.
42+
* @param value The amount of StableToken to mint.
43+
*/
44+
function mint(address to, uint256 value) external returns (bool);
45+
46+
/**
47+
* @notice Burns StableToken from the balance of msg.sender.
48+
* @param value The amount of StableToken to burn.
49+
*/
50+
function burn(uint256 value) external returns (bool);
51+
}

contracts/tokens/StableTokenV3.sol

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3, CalledByVm {
3838
/* ======================== Events ========================= */
3939
/* ========================================================= */
4040

41-
event TransferComment(string comment);
4241
event MinterUpdated(address indexed minter, bool isMinter);
4342
event BurnerUpdated(address indexed burner, bool isBurner);
4443
event OperatorUpdated(address indexed operator, bool isOperator);
@@ -153,12 +152,6 @@ contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3, CalledByVm {
153152
_setBurner(_burner, _isBurner);
154153
}
155154

156-
/// @inheritdoc IStableTokenV3
157-
function transferWithComment(address to, uint256 value, string calldata comment) external returns (bool) {
158-
emit TransferComment(comment);
159-
return transfer(to, value);
160-
}
161-
162155
/// @inheritdoc IStableTokenV3
163156
function mint(address to, uint256 value) external onlyMinter returns (bool) {
164157
_mint(to, value);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
// solhint-disable-next-line max-line-length
4+
import { ERC20PermitUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol";
5+
import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
6+
import { IStableTokenV3CrossChain } from "../interfaces/IStableTokenV3CrossChain.sol";
7+
8+
/**
9+
* @title StableTokenV3CrossChain
10+
* @notice A cross-chain version of the StableTokenV3 contract.
11+
* @dev This contract is used to mint and burn tokens on a different chain.
12+
*/
13+
contract StableTokenV3CrossChain is ERC20PermitUpgradeable, OwnableUpgradeable, IStableTokenV3CrossChain {
14+
/* ========================================================= */
15+
/* ==================== State Variables ==================== */
16+
/* ========================================================= */
17+
18+
// Mapping of allowed addresses that can mint
19+
mapping(address => bool) public isMinter;
20+
// Mapping of allowed addresses that can burn
21+
mapping(address => bool) public isBurner;
22+
23+
/* ========================================================= */
24+
/* ======================== Events ========================= */
25+
/* ========================================================= */
26+
27+
event MinterUpdated(address indexed minter, bool isMinter);
28+
event BurnerUpdated(address indexed burner, bool isBurner);
29+
30+
/* ========================================================= */
31+
/* ====================== Modifiers ======================== */
32+
/* ========================================================= */
33+
34+
modifier onlyMinter() {
35+
address sender = _msgSender();
36+
require(isMinter[sender], "StableTokenV3: not allowed to mint");
37+
_;
38+
}
39+
40+
modifier onlyBurner() {
41+
address sender = _msgSender();
42+
require(isBurner[sender], "StableTokenV3: not allowed to burn");
43+
_;
44+
}
45+
46+
/* ========================================================= */
47+
/* ====================== Constructor ====================== */
48+
/* ========================================================= */
49+
50+
/**
51+
* @notice The constructor for the StableTokenV3 contract.
52+
* @dev Should be called with disable=true in deployments when
53+
* it's accessed through a Proxy.
54+
* Call this with disable=false during testing, when used
55+
* without a proxy.
56+
* @param disable Set to true to run `_disableInitializers()` inherited from
57+
* openzeppelin-contracts-upgradeable/Initializable.sol
58+
*/
59+
constructor(bool disable) {
60+
if (disable) {
61+
_disableInitializers();
62+
}
63+
}
64+
65+
/// @inheritdoc IStableTokenV3CrossChain
66+
function initialize(
67+
string memory name,
68+
string memory symbol,
69+
address[] memory initialBalanceAddresses,
70+
uint256[] memory initialBalanceValues,
71+
address[] memory _minters,
72+
address[] memory _burners
73+
) public initializer {
74+
__ERC20_init(name, symbol);
75+
__ERC20Permit_init(symbol);
76+
_transferOwnership(_msgSender());
77+
78+
require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch");
79+
for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) {
80+
_mint(initialBalanceAddresses[i], initialBalanceValues[i]);
81+
}
82+
for (uint256 i = 0; i < _minters.length; i += 1) {
83+
_setMinter(_minters[i], true);
84+
}
85+
for (uint256 i = 0; i < _burners.length; i += 1) {
86+
_setBurner(_burners[i], true);
87+
}
88+
}
89+
90+
/* ============================================================ */
91+
/* ==================== Mutative Functions ==================== */
92+
/* ============================================================ */
93+
94+
/// @inheritdoc IStableTokenV3CrossChain
95+
function setMinter(address _minter, bool _isMinter) external onlyOwner {
96+
_setMinter(_minter, _isMinter);
97+
}
98+
99+
/// @inheritdoc IStableTokenV3CrossChain
100+
function setBurner(address _burner, bool _isBurner) external onlyOwner {
101+
_setBurner(_burner, _isBurner);
102+
}
103+
104+
/// @inheritdoc IStableTokenV3CrossChain
105+
function mint(address to, uint256 value) external onlyMinter returns (bool) {
106+
_mint(to, value);
107+
return true;
108+
}
109+
110+
/// @inheritdoc IStableTokenV3CrossChain
111+
function burn(uint256 value) external onlyBurner returns (bool) {
112+
_burn(msg.sender, value);
113+
return true;
114+
}
115+
116+
/* =========================================================== */
117+
/* ==================== Private Functions ==================== */
118+
/* =========================================================== */
119+
120+
function _setMinter(address _minter, bool _isMinter) internal {
121+
isMinter[_minter] = _isMinter;
122+
emit MinterUpdated(_minter, _isMinter);
123+
}
124+
125+
function _setBurner(address _burner, bool _isBurner) internal {
126+
isBurner[_burner] = _isBurner;
127+
emit BurnerUpdated(_burner, _isBurner);
128+
}
129+
}

test/unit/tokens/StableTokenV3.t.sol

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Test } from "mento-std/Test.sol";
99
import { StableTokenV3 } from "contracts/tokens/StableTokenV3.sol";
1010

1111
contract StableTokenV3Test is Test {
12-
event TransferComment(string comment);
1312
event Transfer(address indexed from, address indexed to, uint256 value);
1413
event MinterUpdated(address indexed minter, bool isMinter);
1514
event BurnerUpdated(address indexed burner, bool isBurner);
@@ -170,14 +169,6 @@ contract StableTokenV3Test is Test {
170169
token.setBurner(newBurner, true);
171170
}
172171

173-
function test_transferWithComment_shouldEmitCorrectMessage() public {
174-
vm.expectEmit(true, true, true, true);
175-
emit TransferComment("Hello World");
176-
177-
vm.prank(holder0);
178-
token.transferWithComment(holder1, 100, "Hello World");
179-
}
180-
181172
function test_mint_whenCalledByMinter_shouldMintTokens() public {
182173
mintAndAssert(validators, holder0, 100);
183174
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility
3+
// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase
4+
pragma solidity ^0.8;
5+
6+
import { addresses, uints } from "mento-std/Array.sol";
7+
import { Test } from "mento-std/Test.sol";
8+
9+
import { StableTokenV3CrossChain } from "contracts/tokens/StableTokenV3CrossChain.sol";
10+
11+
contract StableTokenV3CrossChainTest is Test {
12+
event MinterUpdated(address indexed minter, bool isMinter);
13+
event BurnerUpdated(address indexed burner, bool isBurner);
14+
15+
address _holder0 = makeAddr("holder0");
16+
address _holder1 = makeAddr("holder1");
17+
address _minter1 = makeAddr("minter1");
18+
address _minter2 = makeAddr("minter2");
19+
address _burner1 = makeAddr("burner1");
20+
address _burner2 = makeAddr("burner2");
21+
22+
StableTokenV3CrossChain private _token;
23+
24+
function setUp() public {
25+
address[] memory minters = new address[](2);
26+
minters[0] = _minter1;
27+
minters[1] = _minter2;
28+
29+
address[] memory burners = new address[](2);
30+
burners[0] = _burner1;
31+
burners[1] = _burner2;
32+
33+
_token = new StableTokenV3CrossChain(false);
34+
_token.initialize(
35+
"cUSD",
36+
"cUSD",
37+
addresses(_holder0, _holder1, _burner1, _minter1),
38+
uints(1000, 1000, 1000, 0),
39+
minters,
40+
burners
41+
);
42+
43+
assertEq(_token.isMinter(_minter1), true);
44+
assertEq(_token.isMinter(_minter2), true);
45+
assertEq(_token.isBurner(_burner1), true);
46+
assertEq(_token.isBurner(_burner2), true);
47+
}
48+
49+
function test_initializers_disabled() public {
50+
StableTokenV3CrossChain disabledToken = new StableTokenV3CrossChain(true);
51+
52+
address[] memory emptyAddresses = new address[](0);
53+
uint256[] memory emptyBalances = new uint256[](0);
54+
address[] memory emptyRoles = new address[](0);
55+
56+
vm.expectRevert(bytes("Initializable: contract is already initialized"));
57+
disabledToken.initialize("cUSD", "cUSD", emptyAddresses, emptyBalances, emptyRoles, emptyRoles);
58+
}
59+
60+
function test_initialize_shouldMintInitialBalances_setRolesAndTransferOwnership() public {
61+
assertEq(_token.balanceOf(_holder0), 1000);
62+
assertEq(_token.balanceOf(_holder1), 1000);
63+
assertEq(_token.balanceOf(_burner1), 1000);
64+
65+
assertEq(_token.owner(), address(this));
66+
}
67+
68+
function test_setMinter_whenCalledByOwner_shouldSetMinterAndEmitEvent() public {
69+
address newMinter = makeAddr("newMinter");
70+
vm.expectEmit(true, true, true, true);
71+
emit MinterUpdated(newMinter, true);
72+
_token.setMinter(newMinter, true);
73+
assertEq(_token.isMinter(newMinter), true);
74+
75+
vm.expectEmit(true, true, true, true);
76+
emit MinterUpdated(newMinter, false);
77+
_token.setMinter(newMinter, false);
78+
assertEq(_token.isMinter(newMinter), false);
79+
}
80+
81+
function test_setMinter_whenCalledByNotOwner_shouldRevert() public {
82+
address newMinter = makeAddr("newMinter");
83+
vm.prank(_holder0);
84+
vm.expectRevert(bytes("Ownable: caller is not the owner"));
85+
_token.setMinter(newMinter, true);
86+
}
87+
88+
function test_setBurner_whenCalledByOwner_shouldSetBurnerAndEmitEvent() public {
89+
address newBurner = makeAddr("newBurner");
90+
vm.expectEmit(true, true, true, true);
91+
emit BurnerUpdated(newBurner, true);
92+
_token.setBurner(newBurner, true);
93+
assertEq(_token.isBurner(newBurner), true);
94+
95+
vm.expectEmit(true, true, true, true);
96+
emit BurnerUpdated(newBurner, false);
97+
_token.setBurner(newBurner, false);
98+
assertEq(_token.isBurner(newBurner), false);
99+
}
100+
101+
function test_setBurner_whenCalledByNotOwner_shouldRevert() public {
102+
address newBurner = makeAddr("newBurner");
103+
vm.prank(_holder0);
104+
vm.expectRevert(bytes("Ownable: caller is not the owner"));
105+
_token.setBurner(newBurner, true);
106+
}
107+
108+
function test_mint_whenCalledByMinter_shouldMintTokens() public {
109+
uint256 balanceBefore = _token.balanceOf(_holder0);
110+
vm.prank(_minter1);
111+
bool ok = _token.mint(_holder0, 100);
112+
assertTrue(ok);
113+
assertEq(_token.balanceOf(_holder0), balanceBefore + 100);
114+
}
115+
116+
function test_mint_whenSenderIsNotAuthorized_shouldRevert() public {
117+
vm.prank(_holder0);
118+
vm.expectRevert(bytes("StableTokenV3: not allowed to mint"));
119+
_token.mint(_holder0, 100);
120+
}
121+
122+
function test_burn_whenCalledByBurner_shouldBurnTokens() public {
123+
uint256 balanceBefore = _token.balanceOf(_burner1);
124+
vm.prank(_burner1);
125+
bool ok = _token.burn(100);
126+
assertTrue(ok);
127+
assertEq(_token.balanceOf(_burner1), balanceBefore - 100);
128+
}
129+
130+
function test_burn_whenSenderIsNotAuthorized_shouldRevert() public {
131+
vm.prank(_holder0);
132+
vm.expectRevert(bytes("StableTokenV3: not allowed to burn"));
133+
_token.burn(100);
134+
}
135+
}

0 commit comments

Comments
 (0)