Skip to content

Commit 2f9a2ba

Browse files
authored
[CCTPE-564] Add minimum fee (circlefin#68)
- Add `minFee` variable to store the minimum fee (in 1/1,000th basis points) for all transfers and enforcement logic. - Add `minFeeController` role for managing `minFee`. - Add getter and setter for new variable. - Add getter and setter for new role. - Add and update relevant tests. - Update deployment scripts and tests. Audit scope: https://docs.google.com/document/d/129HaTPi1MojGf8zd8v657zkctSmFD87wjTf9naQVfkA/edit?tab=t.0
1 parent dec8ddd commit 2f9a2ba

12 files changed

+1057
-293
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ The proxies are deployed via `CREATE2` through Create2Factory. The scripts assum
162162
- `TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS`
163163
- `TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS`
164164
- `TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS`
165+
- `TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS`
166+
- `TOKEN_MESSENGER_V2_MIN_FEE`
165167

166168
- `DOMAIN`
167169
- `BURN_LIMIT_PER_MESSAGE`

anvil/crosschainTransferITV2.py

Lines changed: 454 additions & 186 deletions
Large diffs are not rendered by default.

scripts/v2/DeployProxiesV2.s.sol

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ contract DeployProxiesV2Script is Script {
6060
address private tokenMessengerV2FeeRecipientAddress;
6161
address private tokenMessengerV2DenylisterAddress;
6262
address private tokenMessengerV2AdminAddress;
63+
address private tokenMessengerV2MinFeeControllerAddress;
64+
uint256 private tokenMessengerV2MinFee;
6365

6466
uint32 private domain;
6567
uint32 private version;
@@ -185,11 +187,15 @@ contract DeployProxiesV2Script is Script {
185187
}
186188
bytes memory initializer = abi.encodeWithSelector(
187189
TokenMessengerV2.initialize.selector,
188-
tokenMessengerV2OwnerAddress,
189-
tokenMessengerV2RescuerAddress,
190-
tokenMessengerV2FeeRecipientAddress,
191-
tokenMessengerV2DenylisterAddress,
192-
address(tokenMinterV2),
190+
TokenMessengerV2.TokenMessengerV2Roles({
191+
owner: tokenMessengerV2OwnerAddress,
192+
rescuer: tokenMessengerV2RescuerAddress,
193+
feeRecipient: tokenMessengerV2FeeRecipientAddress,
194+
denylister: tokenMessengerV2DenylisterAddress,
195+
tokenMinter: address(tokenMinterV2),
196+
minFeeController: tokenMessengerV2MinFeeControllerAddress
197+
}),
198+
tokenMessengerV2MinFee,
193199
remoteDomains,
194200
remoteTokenMessengerAddresses
195201
);
@@ -333,6 +339,10 @@ contract DeployProxiesV2Script is Script {
333339
tokenMessengerV2AdminAddress = vm.envAddress(
334340
"TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS"
335341
);
342+
tokenMessengerV2MinFeeControllerAddress = vm.envAddress(
343+
"TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS"
344+
);
345+
tokenMessengerV2MinFee = vm.envUint("TOKEN_MESSENGER_V2_MIN_FEE");
336346

337347
domain = uint32(vm.envUint("DOMAIN"));
338348
version = uint32(vm.envUint("VERSION"));

src/v2/BaseTokenMessenger.sol

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
6262
*/
6363
event FeeRecipientSet(address feeRecipient);
6464

65+
/**
66+
* @notice Emitted when the minimum fee controller is set
67+
* @param minFeeController address of minimum fee controller
68+
*/
69+
event MinFeeControllerSet(address minFeeController);
70+
71+
/**
72+
* @notice Emitted when the minimum fee is set
73+
* @param minFee minimum fee
74+
*/
75+
event MinFeeSet(uint256 minFee);
76+
6577
/**
6678
* @notice Emitted when tokens are minted
6779
* @param mintRecipient recipient address of minted tokens
@@ -92,6 +104,15 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
92104
// Address to receive collected fees
93105
address public feeRecipient;
94106

107+
// Minimum fee controller address
108+
address public minFeeController;
109+
110+
// Minimum fee for all transfers in 1/1000 basis points
111+
uint256 public minFee;
112+
113+
// Minimum fee multiplier to support 1/1000 basis point precision
114+
uint256 public constant MIN_FEE_MULTIPLIER = 10_000_000;
115+
95116
// ============ Modifiers ============
96117
/**
97118
* @notice Only accept messages from a registered TokenMessenger contract on given remote domain
@@ -115,6 +136,17 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
115136
_;
116137
}
117138

139+
/**
140+
* @notice Reverts if called by any account other than the min fee controller
141+
*/
142+
modifier onlyMinFeeController() {
143+
require(
144+
msg.sender == minFeeController,
145+
"Caller is not the min fee controller"
146+
);
147+
_;
148+
}
149+
118150
// ============ Constructor ============
119151
/**
120152
* @param _messageTransmitter Message transmitter address
@@ -191,6 +223,26 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
191223
_setFeeRecipient(_feeRecipient);
192224
}
193225

226+
/**
227+
* @notice Sets the minimum fee controller address
228+
* @dev Reverts if not called by the owner
229+
* @dev Reverts if `_minFeeController` is the zero address
230+
* @param _minFeeController Address of minimum fee controller
231+
*/
232+
function setMinFeeController(address _minFeeController) external onlyOwner {
233+
_setMinFeeController(_minFeeController);
234+
}
235+
236+
/**
237+
* @notice Sets the minimum fee for all transfers in 1/1000 basis points
238+
* @dev Reverts if not called by the min fee controller
239+
* @dev Reverts if the minimum fee is equal to or greater than MIN_FEE_MULTIPLIER
240+
* @param _minFee Minimum fee
241+
*/
242+
function setMinFee(uint256 _minFee) external onlyMinFeeController {
243+
_setMinFee(_minFee);
244+
}
245+
194246
/**
195247
* @notice Returns the current initialized version
196248
*/
@@ -335,6 +387,28 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
335387
emit LocalMinterAdded(_newLocalMinter);
336388
}
337389

390+
/**
391+
* @notice Sets the minimum fee controller address
392+
* @dev Reverts if `_minFeeController` is the zero address
393+
* @param _minFeeController Address of minimum fee controller
394+
*/
395+
function _setMinFeeController(address _minFeeController) internal {
396+
require(_minFeeController != address(0), "Zero address not allowed");
397+
minFeeController = _minFeeController;
398+
emit MinFeeControllerSet(_minFeeController);
399+
}
400+
401+
/**
402+
* @notice Sets the minimum fee for all transfers
403+
* @dev Reverts if the minimum fee is equal to or greater than MIN_FEE_MULTIPLIER
404+
* @param _minFee Minimum fee
405+
*/
406+
function _setMinFee(uint256 _minFee) internal {
407+
require(_minFee < MIN_FEE_MULTIPLIER, "Min fee too high");
408+
minFee = _minFee;
409+
emit MinFeeSet(_minFee);
410+
}
411+
338412
/**
339413
* @notice Add the TokenMessenger for a remote domain.
340414
* @dev Reverts if there is already a TokenMessenger set for domain.

src/v2/TokenMessengerV2.sol

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
* limitations under the License.
1717
*/
1818
pragma solidity 0.7.6;
19+
pragma abicoder v2;
1920

21+
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
2022
import {BaseTokenMessenger} from "./BaseTokenMessenger.sol";
2123
import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol";
2224
import {AddressUtils} from "../messages/v2/AddressUtils.sol";
@@ -32,6 +34,16 @@ import {TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD} from "./FinalityThresholds.sol";
3234
* and to/from TokenMinters.
3335
*/
3436
contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
37+
// ============ Structs ============
38+
struct TokenMessengerV2Roles {
39+
address owner;
40+
address rescuer;
41+
address feeRecipient;
42+
address denylister;
43+
address tokenMinter;
44+
address minFeeController;
45+
}
46+
3547
// ============ Events ============
3648
/**
3749
* @notice Emitted when a DepositForBurn message is sent
@@ -67,6 +79,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
6779
using BurnMessageV2 for bytes29;
6880
using TypedMemView for bytes;
6981
using TypedMemView for bytes29;
82+
using SafeMath for uint256;
7083

7184
// ============ Constructor ============
7285
/**
@@ -83,46 +96,40 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
8396
// ============ Initializers ============
8497
/**
8598
* @notice Initializes the contract
86-
* @dev Reverts if `owner_` is the zero address
87-
* @dev Reverts if `rescuer_` is the zero address
88-
* @dev Reverts if `feeRecipient_` is the zero address
89-
* @dev Reverts if `denylister_` is the zero address
90-
* @dev Reverts if `tokenMinter_` is the zero address
99+
* @dev Reverts if any of the roles are the zero address
91100
* @dev Reverts if `remoteDomains_` and `remoteTokenMessengers_` are unequal length
92101
* @dev Each remoteTokenMessenger address must correspond to the remote domain at the same
93102
* index in respective arrays.
94103
* @dev Reverts if any `remoteTokenMessengers_` entry equals bytes32(0)
95-
* @param owner_ Owner address
96-
* @param rescuer_ Rescuer address
97-
* @param feeRecipient_ FeeRecipient address
98-
* @param denylister_ Denylister address
99-
* @param tokenMinter_ Local token minter address
104+
* @param roles Roles configuration
105+
* @param minFee_ Minimum fee
100106
* @param remoteDomains_ Array of remote domains to configure
101107
* @param remoteTokenMessengers_ Array of remote token messenger addresses
102108
*/
103109
function initialize(
104-
address owner_,
105-
address rescuer_,
106-
address feeRecipient_,
107-
address denylister_,
108-
address tokenMinter_,
110+
TokenMessengerV2Roles calldata roles,
111+
uint256 minFee_,
109112
uint32[] calldata remoteDomains_,
110113
bytes32[] calldata remoteTokenMessengers_
111114
) external initializer {
112-
require(owner_ != address(0), "Owner is the zero address");
115+
require(roles.owner != address(0), "Owner is the zero address");
113116
require(
114117
remoteDomains_.length == remoteTokenMessengers_.length,
115118
"Invalid remote domain configuration"
116119
);
117120

118121
// Roles
119-
_transferOwnership(owner_);
120-
_updateRescuer(rescuer_);
121-
_updateDenylister(denylister_);
122-
_setFeeRecipient(feeRecipient_);
122+
_transferOwnership(roles.owner);
123+
_updateRescuer(roles.rescuer);
124+
_updateDenylister(roles.denylister);
125+
_setFeeRecipient(roles.feeRecipient);
123126

124127
// Local minter configuration
125-
_setLocalMinter(tokenMinter_);
128+
_setLocalMinter(roles.tokenMinter);
129+
130+
// Fee configuration
131+
_setMinFeeController(roles.minFeeController);
132+
_setMinFee(minFee_);
126133

127134
// Remote token messenger configuration
128135
uint256 _remoteDomainsLength = remoteDomains_.length;
@@ -145,6 +152,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
145152
* to this contract is less than `amount`.
146153
* - burn() reverts. For example, if `amount` is 0.
147154
* - maxFee is greater than or equal to `amount`.
155+
* - maxFee is less than `amount * minFee / MIN_FEE_MULTIPLIER`.
148156
* - MessageTransmitterV2#sendMessage reverts.
149157
* @param amount amount of tokens to burn
150158
* @param destinationDomain destination domain to receive message on
@@ -188,6 +196,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
188196
* to this contract is less than `amount`.
189197
* - burn() reverts. For example, if `amount` is 0.
190198
* - maxFee is greater than or equal to `amount`.
199+
* - maxFee is less than `amount * minFee / MIN_FEE_MULTIPLIER`.
191200
* - MessageTransmitterV2#sendMessage reverts.
192201
* @param amount amount of tokens to burn
193202
* @param destinationDomain destination domain to receive message on
@@ -282,7 +291,33 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
282291
return _handleReceiveMessage(messageBody.ref(0), remoteDomain);
283292
}
284293

294+
/**
295+
* @notice Returns the minimum fee for a given amount
296+
* @param amount The amount for which to calculate the minimum fee
297+
* @return The minimum fee for the given amount
298+
*/
299+
function getMinFeeAmount(uint256 amount) external view returns (uint256) {
300+
if (minFee == 0) return 0;
301+
302+
require(amount > 1, "Amount too low");
303+
return _calcMinFeeAmount(amount);
304+
}
305+
285306
// ============ Internal Utils ============
307+
/**
308+
* Calculates the minimum fee amount for a given amount.
309+
* @dev Amount should be constrained to be greater than 1.
310+
* @dev Assumes `minFee` is non-zero.
311+
* @param _amount The amount for which to calculate the minimum fee.
312+
* @return The minimum fee for the given amount.
313+
*/
314+
function _calcMinFeeAmount(
315+
uint256 _amount
316+
) internal view returns (uint256) {
317+
uint256 _minFeeAmount = _amount.mul(minFee) / MIN_FEE_MULTIPLIER;
318+
return _minFeeAmount == 0 ? 1 : _minFeeAmount;
319+
}
320+
286321
/**
287322
* @notice Deposits and burns tokens from sender to be minted on destination domain.
288323
* Emits a `DepositForBurn` event.
@@ -308,6 +343,16 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
308343
require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero");
309344
require(_maxFee < _amount, "Max fee must be less than amount");
310345

346+
// Verify minimum fee
347+
if (minFee > 0) {
348+
// Implicitly constrains `_amount` to be greater than 1
349+
// 0 < minFeeAmount <= maxFee < amount
350+
require(
351+
_maxFee >= _calcMinFeeAmount(_amount),
352+
"Insufficient max fee"
353+
);
354+
}
355+
311356
bytes32 _destinationTokenMessenger = _getRemoteTokenMessenger(
312357
_destinationDomain
313358
);

test/TestUtils.sol

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ contract TestUtils is Test {
8383
address newTokenController = vm.addr(1900);
8484
address owner = vm.addr(1902);
8585
address arbitraryAddress = vm.addr(1903);
86+
uint256 internal constant MIN_FEE_MULTIPLIER = 10_000_000;
8687

8788
// See: https://github.com/foundry-rs/foundry/blob/2cdbfaca634b284084d0f86357623aef7a0d2ce3/crates/evm/core/src/constants.rs#L9
8889
// This address may be passed into fuzz tests by Foundry. VM.mockCalls fail when
@@ -513,4 +514,51 @@ contract TestUtils is Test {
513514

514515
return _signaturesConcatenated;
515516
}
517+
518+
/**
519+
* Returns the minimum fee amount for a given amount and minimum fee.
520+
* @param _amount The amount to calculate the minimum fee for
521+
* @param _minFee The minimum fee
522+
* @return minFeeAmount The minimum fee amount
523+
*/
524+
function _getMinFeeAmount(
525+
uint256 _amount,
526+
uint256 _minFee
527+
) internal pure returns (uint256 minFeeAmount) {
528+
if (_minFee == 0) return 0;
529+
minFeeAmount = (_amount * _minFee) / MIN_FEE_MULTIPLIER;
530+
minFeeAmount = minFeeAmount == 0 ? 1 : minFeeAmount;
531+
}
532+
533+
/**
534+
* Helper function to prevent overflow. Takes into account no minimum fee.
535+
* @param _amount The amount to bound
536+
* @param _minFee The minimum fee
537+
* @return The bounded amount
538+
*/
539+
function _boundAmountForMinFee(
540+
uint256 _amount,
541+
uint256 _minFee
542+
) internal pure returns (uint256) {
543+
return
544+
bound(
545+
_amount,
546+
_minFee == 0 ? 1 : 2,
547+
_minFee == 0 ? type(uint256).max : type(uint256).max / _minFee
548+
);
549+
}
550+
551+
/**
552+
* Helper function to cause overflow. Assumes non-zero minimum fee.
553+
* @param _amount The amount to bound
554+
* @param _minFee The minimum fee
555+
* @return The bounded amount
556+
*/
557+
function _boundOverflowAmountForMinFee(
558+
uint256 _amount,
559+
uint256 _minFee
560+
) internal pure returns (uint256) {
561+
return
562+
bound(_amount, type(uint256).max / _minFee + 1, type(uint256).max);
563+
}
516564
}

test/mocks/v2/MockTokenMessengerV3.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* limitations under the License.
1717
*/
1818
pragma solidity 0.7.6;
19+
pragma abicoder v2;
1920

2021
import {TokenMessengerV2} from "../../../src/v2/TokenMessengerV2.sol";
2122

0 commit comments

Comments
 (0)