Skip to content

Commit 7be9593

Browse files
authored
feat: ERC4626 Exchange Rate Threshold (SC-1229) (#186)
* feat: ERC4626 Exchange Rate Threshold (SC-1229) * fix: PR review * feat: simpler setting and checking * fix: PR Reviews * refactor: error messages * fix: PR review * fix: PR Review * fix: alphabetical * feat: zero rate deposit failure tests
1 parent f529c06 commit 7be9593

30 files changed

+580
-267
lines changed

src/ForeignController.sol

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
4949
);
5050

5151
event LayerZeroRecipientSet(uint32 indexed destinationEndpointId, bytes32 layerZeroRecipient);
52+
event MaxExchangeRateSet(address indexed token, uint256 maxExchangeRate);
5253
event MaxSlippageSet(address indexed pool, uint256 maxSlippage);
5354
event MintRecipientSet(uint32 indexed destinationDomain, bytes32 mintRecipient);
5455
event RelayerRemoved(address indexed relayer);
@@ -57,6 +58,8 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
5758
/*** State variables ***/
5859
/**********************************************************************************************/
5960

61+
uint256 public constant EXCHANGE_RATE_PRECISION = 1e36;
62+
6063
bytes32 public constant FREEZER = keccak256("FREEZER");
6164
bytes32 public constant RELAYER = keccak256("RELAYER");
6265

@@ -84,6 +87,9 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
8487
mapping(uint32 destinationDomain => bytes32 mintRecipient) public mintRecipients;
8588
mapping(uint32 destinationEndpointId => bytes32 layerZeroRecipient) public layerZeroRecipients;
8689

90+
// ERC4626 exchange rate thresholds (1e36 precision)
91+
mapping(address token => uint256 maxExchangeRate) public maxExchangeRates;
92+
8793
/**********************************************************************************************/
8894
/*** Initialization ***/
8995
/**********************************************************************************************/
@@ -122,7 +128,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
122128
modifier rateLimitExists(bytes32 key) {
123129
require(
124130
rateLimits.getRateLimitData(key).maxAmount > 0,
125-
"ForeignController/invalid-action"
131+
"FC/invalid-action"
126132
);
127133
_;
128134
}
@@ -134,7 +140,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
134140
function setMaxSlippage(address pool, uint256 maxSlippage)
135141
external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE)
136142
{
137-
require(pool != address(0), "ForeignController/pool-zero-address");
143+
require(pool != address(0), "FC/pool-zero-address");
138144

139145
maxSlippages[pool] = maxSlippage;
140146
emit MaxSlippageSet(pool, maxSlippage);
@@ -154,6 +160,19 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
154160
emit LayerZeroRecipientSet(destinationEndpointId, layerZeroRecipient);
155161
}
156162

163+
function setMaxExchangeRate(address token, uint256 shares, uint256 maxExpectedAssets)
164+
external nonReentrant
165+
{
166+
_checkRole(DEFAULT_ADMIN_ROLE);
167+
168+
require(token != address(0), "FC/token-zero-address");
169+
170+
emit MaxExchangeRateSet(
171+
token,
172+
maxExchangeRates[token] = _getExchangeRate(shares, maxExpectedAssets)
173+
);
174+
}
175+
157176
/**********************************************************************************************/
158177
/*** Freezer functions ***/
159178
/**********************************************************************************************/
@@ -182,7 +201,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
182201

183202
require(
184203
returnData.length == 0 || abi.decode(returnData, (bool)),
185-
"ForeignController/transfer-failed"
204+
"FC/transfer-failed"
186205
);
187206
}
188207

@@ -252,7 +271,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
252271
{
253272
bytes32 mintRecipient = mintRecipients[destinationDomain];
254273

255-
require(mintRecipient != 0, "ForeignController/domain-not-configured");
274+
require(mintRecipient != 0, "FC/domain-not-configured");
256275

257276
// Approve USDC to CCTP from the proxy (assumes the proxy has enough USDC).
258277
_approve(address(usdc), address(cctp), usdcAmount);
@@ -330,13 +349,8 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
330349
rateLimitedAddress(LIMIT_4626_DEPOSIT, token, amount)
331350
returns (uint256 shares)
332351
{
333-
require(maxSlippages[token] != 0, "ForeignController/max-slippage-not-set");
334-
335-
// Note that whitelist is done by rate limits.
336-
IERC20 asset = IERC20(IERC4626(token).asset());
337-
338352
// Approve asset to token from the proxy (assumes the proxy has enough of the asset).
339-
_approve(address(asset), token, amount);
353+
_approve(IERC4626(token).asset(), token, amount);
340354

341355
// Deposit asset into the token, proxy receives token shares, decode the resulting shares.
342356
shares = abi.decode(
@@ -348,8 +362,8 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
348362
);
349363

350364
require(
351-
IERC4626(token).convertToAssets(shares) >= amount * maxSlippages[token] / 1e18,
352-
"ForeignController/inflated-shares"
365+
_getExchangeRate(shares, amount) <= maxExchangeRates[token],
366+
"FC/exchange-rate-too-high"
353367
);
354368
}
355369

@@ -410,7 +424,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
410424
onlyRole(RELAYER)
411425
rateLimitedAddress(LIMIT_AAVE_DEPOSIT, aToken, amount)
412426
{
413-
require(maxSlippages[aToken] != 0, "ForeignController/max-slippage-not-set");
427+
require(maxSlippages[aToken] != 0, "FC/max-slippage-not-set");
414428

415429
IERC20 underlying = IERC20(IATokenWithPool(aToken).UNDERLYING_ASSET_ADDRESS());
416430
IAavePool pool = IAavePool(IATokenWithPool(aToken).POOL());
@@ -430,7 +444,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
430444

431445
require(
432446
newATokens >= amount * maxSlippages[aToken] / 1e18,
433-
"ForeignController/slippage-too-high"
447+
"FC/slippage-too-high"
434448
);
435449
}
436450

@@ -553,7 +567,7 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
553567
// Revert if approve returns false
554568
require(
555569
approveCallReturnData.length == 0 || abi.decode(approveCallReturnData, (bool)),
556-
"ForeignController/approve-failed"
570+
"FC/approve-failed"
557571
);
558572
}
559573

@@ -587,4 +601,18 @@ contract ForeignController is ReentrancyGuard, AccessControlEnumerable {
587601
rateLimits.triggerRateLimitDecrease(key, amount);
588602
}
589603

604+
/**********************************************************************************************/
605+
/*** Exchange rate helper functions ***/
606+
/**********************************************************************************************/
607+
608+
function _getExchangeRate(uint256 shares, uint256 assets) internal pure returns (uint256) {
609+
// Return 0 for zero assets first, to handle the valid case of 0 shares and 0 assets.
610+
if (assets == 0) return 0;
611+
612+
// Zero shares with non-zero assets is invalid (infinite exchange rate).
613+
if (shares == 0) revert("FC/zero-shares");
614+
615+
return (EXCHANGE_RATE_PRECISION * assets) / shares;
616+
}
617+
590618
}

0 commit comments

Comments
 (0)