@@ -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