From 4b950bcbf13928d01179b5fb3e3fff9cdac05219 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Fri, 13 Sep 2024 16:47:27 +0100 Subject: [PATCH 1/2] Add logic to allow cross-l2 bridging --- script/DeployGydL1CCIPEscrow.s.sol | 7 +- src/GydL1CCIPEscrow.sol | 57 ++--------- src/IGydBridge.sol | 49 +++++++++ src/L2Gyd.sol | 159 ++++++++++++++++------------- test/GydL1Escrow.t.sol | 7 +- test/L2Gyd.t.sol | 61 ++++++----- 6 files changed, 180 insertions(+), 160 deletions(-) create mode 100644 src/IGydBridge.sol diff --git a/script/DeployGydL1CCIPEscrow.s.sol b/script/DeployGydL1CCIPEscrow.s.sol index 85a6d86..bea4330 100644 --- a/script/DeployGydL1CCIPEscrow.s.sol +++ b/script/DeployGydL1CCIPEscrow.s.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import "forge-std/Script.sol"; import {GydL1CCIPEscrow} from "src/GydL1CCIPEscrow.sol"; +import {IGydBridge} from "src/IGydBridge.sol"; import {UUPSProxy} from "./UUPSProxy.sol"; import {ICREATE3Factory} from "./ICREATE3Factory.sol"; @@ -55,10 +56,10 @@ contract DeployGydL1CCIPEscrow is Script { // Only support Arbitrum chain on deployment GydL1CCIPEscrow.ChainData[] memory chains = new GydL1CCIPEscrow.ChainData[](1); - chains[0] = GydL1CCIPEscrow.ChainData({ + chains[0] = IGydBridge.ChainData({ chainSelector: arbitrumChainSelector, - metadata: GydL1CCIPEscrow.ChainMetadata({ - gydAddress: l2Address, + metadata: IGydBridge.ChainMetadata({ + targetAddress: l2Address, gasLimit: gasLimit }) }); diff --git a/src/GydL1CCIPEscrow.sol b/src/GydL1CCIPEscrow.sol index 4cc6bec..55a6d82 100644 --- a/src/GydL1CCIPEscrow.sol +++ b/src/GydL1CCIPEscrow.sol @@ -12,6 +12,7 @@ import {IRouterClient} from "ccip/interfaces/IRouterClient.sol"; import {Client} from "ccip/libraries/Client.sol"; import {CCIPReceiverUpgradeable} from "./CCIPReceiverUpgradeable.sol"; +import {IGydBridge} from "./IGydBridge.sol"; import {CCIPHelpers} from "./CCIPHelpers.sol"; /** @@ -19,6 +20,7 @@ import {CCIPHelpers} from "./CCIPHelpers.sol"; * @notice Main smart contract to bridge GYD from Ethereum using Chainlink CCIP */ contract GydL1CCIPEscrow is + IGydBridge, Initializable, UUPSUpgradeable, AccessControlDefaultAdminRulesUpgradeable, @@ -28,16 +30,6 @@ contract GydL1CCIPEscrow is using Address for address; using Address for address payable; - struct ChainMetadata { - address gydAddress; - uint256 gasLimit; - } - - struct ChainData { - uint64 chainSelector; - ChainMetadata metadata; - } - /// @notice GYD contract IERC20 public gyd; @@ -52,39 +44,6 @@ contract GydL1CCIPEscrow is /// @notice The total amount of GYD bridged per chain mapping(uint64 => uint256) public totalBridgedGYD; - /// @notice This event is emitted when a new chain is added - event ChainAdded( - uint64 indexed chainSelector, address indexed gydAddress, uint256 gasLimit - ); - - /// @notice This event is emitted when the gas limit is updated - event GasLimitUpdated(uint64 indexed chainSelector, uint256 gasLimit); - - /// @notice This event is emitted when the GYD is bridged - event GYDBridged( - uint64 indexed chainSelector, - address indexed bridger, - uint256 amount, - uint256 total - ); - - /// @notice This event is emitted when the GYD is claimed - event GYDClaimed( - uint64 indexed chainSelector, - address indexed bridger, - uint256 amount, - uint256 total - ); - - /// @notice This error is raised if message from the bridge is invalid - error MessageInvalid(); - - /// @notice This error is raised if the chain is not supported - error ChainNotSupported(uint64 chainSelector); - - /// @notice This error is raised if the msg value is not enough for the fees - error FeesNotCovered(uint256 fees); - /// @notice Disable initializer on deploy constructor() { _disableInitializers(); @@ -112,7 +71,7 @@ contract GydL1CCIPEscrow is chainsMetadata[chains[i].chainSelector] = chains[i].metadata; emit ChainAdded( chains[i].chainSelector, - chains[i].metadata.gydAddress, + chains[i].metadata.targetAddress, chains[i].metadata.gasLimit ); } @@ -179,12 +138,12 @@ contract GydL1CCIPEscrow is gyd.safeTransferFrom(msg.sender, address(this), amount); ChainMetadata memory chainMeta = chainsMetadata[destinationChainSelector]; - if (chainMeta.gydAddress == address(0)) { + if (chainMeta.targetAddress == address(0)) { revert ChainNotSupported(destinationChainSelector); } Client.EVM2AnyMessage memory evm2AnyMessage = CCIPHelpers.buildCCIPMessage( - chainMeta.gydAddress, recipient, amount, data, chainMeta.gasLimit + chainMeta.targetAddress, recipient, amount, data, chainMeta.gasLimit ); uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage); CCIPHelpers.sendCCIPMessage( @@ -212,12 +171,12 @@ contract GydL1CCIPEscrow is bytes memory data ) public view returns (uint256) { ChainMetadata memory chainMeta = chainsMetadata[destinationChainSelector]; - if (chainMeta.gydAddress == address(0)) { + if (chainMeta.targetAddress == address(0)) { revert ChainNotSupported(destinationChainSelector); } Client.EVM2AnyMessage memory evm2AnyMessage = CCIPHelpers.buildCCIPMessage( - chainMeta.gydAddress, recipient, amount, data, chainMeta.gasLimit + chainMeta.targetAddress, recipient, amount, data, chainMeta.gasLimit ); return router.getFee(destinationChainSelector, evm2AnyMessage); } @@ -240,7 +199,7 @@ contract GydL1CCIPEscrow is override { address expectedSender = - chainsMetadata[any2EvmMessage.sourceChainSelector].gydAddress; + chainsMetadata[any2EvmMessage.sourceChainSelector].targetAddress; if (expectedSender == address(0)) { revert ChainNotSupported(any2EvmMessage.sourceChainSelector); } diff --git a/src/IGydBridge.sol b/src/IGydBridge.sol new file mode 100644 index 0000000..1fb3045 --- /dev/null +++ b/src/IGydBridge.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IGydBridge { + struct ChainMetadata { + address targetAddress; + uint256 gasLimit; + } + + struct ChainData { + uint64 chainSelector; + ChainMetadata metadata; + } + + /// @notice This event is emitted when a new chain is added + event ChainAdded( + uint64 indexed chainSelector, + address indexed targetAddress, + uint256 gasLimit + ); + + /// @notice This event is emitted when the gas limit is updated + event GasLimitUpdated(uint64 indexed chainSelector, uint256 gasLimit); + + /// @notice This event is emitted when the GYD is bridged + event GYDBridged( + uint64 indexed chainSelector, + address indexed bridger, + uint256 amount, + uint256 total + ); + + /// @notice This event is emitted when the GYD is claimed + event GYDClaimed( + uint64 indexed chainSelector, + address indexed bridger, + uint256 amount, + uint256 total + ); + + /// @notice This error is raised if message from the bridge is invalid + error MessageInvalid(); + + /// @notice This error is raised if the chain is not supported + error ChainNotSupported(uint64 chainSelector); + + /// @notice This error is raised if the msg value is not enough for the fees + error FeesNotCovered(uint256 fees); +} diff --git a/src/L2Gyd.sol b/src/L2Gyd.sol index b0efd6c..297d899 100644 --- a/src/L2Gyd.sol +++ b/src/L2Gyd.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import {EnumerableMap} from "oz/utils/structs/EnumerableMap.sol"; import {Address} from "oz/utils/Address.sol"; import {Initializable} from "upgradeable/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -12,6 +13,7 @@ import {Client} from "ccip/libraries/Client.sol"; import {IRouterClient} from "ccip/interfaces/IRouterClient.sol"; import {CCIPReceiverUpgradeable} from "./CCIPReceiverUpgradeable.sol"; import {CCIPHelpers} from "./CCIPHelpers.sol"; +import {IGydBridge} from "./IGydBridge.sol"; /** * @title L2Gyd @@ -19,6 +21,7 @@ import {CCIPHelpers} from "./CCIPHelpers.sol"; * back GYD too */ contract L2Gyd is + IGydBridge, Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, @@ -30,26 +33,10 @@ contract L2Gyd is /// @notice The CCIP router contract IRouterClient public router; - /// @notice GydL1Escrow contract address on Ethereum mainnet - address public destAddress; - - /// @notice Gas limit when bridging to the GydL1Escrow - uint256 public bridgeGasLimit; - - /// @notice Chain selector of Ethereum mainnet on CCIP - uint64 public mainnetChainSelector; - - /// @notice This event is emitted when the GYD is bridged - event GYDBridged(address indexed bridger, uint256 amount, uint256 total); - - /// @notice This event is emitted when the GYD is claimed - event GYDClaimed(address indexed bridger, uint256 amount, uint256 total); - - /// @notice This event is emitted when the gas limit is updated - event GasLimitUpdated(uint256 gasLimit); - - /// @notice This error is raised if message from the bridge is invalid - error MessageInvalid(); + /// @notice Mapping from chain selector to chain metadata (mainly GYD + /// contract address) + /// Only chains in this mapping can be bridged to + mapping(uint64 => ChainMetadata) public chainsMetadata; /// @notice This error is raised if ownership is renounced error RenounceInvalid(); @@ -64,17 +51,11 @@ contract L2Gyd is * @dev This initializer should be called via UUPSProxy constructor * @param _ownerAddress The contract owner * @param _routerAddress The CCIP router address - * @param _destAddress The contract address of GydL1Escrow - * @param _chainSelector The chain selector of Ethereum mainnet on CCIP - * @param _bridgeGasLimit The gas limit when bridging to the GydL1Escrow */ - function initialize( - address _ownerAddress, - address _routerAddress, - address _destAddress, - uint64 _chainSelector, - uint256 _bridgeGasLimit - ) public initializer { + function initialize(address _ownerAddress, address _routerAddress) + public + initializer + { __Ownable2Step_init(); __UUPSUpgradeable_init(); __ERC20_init("Gyro Dollar", "GYD"); @@ -82,9 +63,6 @@ contract L2Gyd is _transferOwnership(_ownerAddress); router = IRouterClient(_routerAddress); - destAddress = _destAddress; - mainnetChainSelector = _chainSelector; - bridgeGasLimit = _bridgeGasLimit; } /** @@ -101,58 +79,94 @@ contract L2Gyd is revert RenounceInvalid(); } - function bridgeToken(address recipient, uint256 amount) public payable { - bridgeToken(recipient, amount, ""); + /** + * @notice Allows the owner to support a new chain + * @param chainSelector the selector of the chain + * https://docs.chain.link/ccip/supported-networks/v1_2_0/mainnet#configuration + * @param gydAddress the GYD contract address on the chain + */ + function addChain(uint64 chainSelector, address gydAddress, uint256 gasLimit) + external + onlyOwner + { + chainsMetadata[chainSelector] = ChainMetadata(gydAddress, gasLimit); + emit ChainAdded(chainSelector, gydAddress, gasLimit); } /** - * @notice Bridge GYD from the current chain to Ethereum mainnet + * Updates the gas limit when bridging to a chain + * @param chainSelector the selector of the chain + * @param gasLimit the new gas limit for this chain + */ + function updateGasLimit(uint64 chainSelector, uint256 gasLimit) + external + onlyOwner + { + ChainMetadata storage chainMetadata = chainsMetadata[chainSelector]; + chainMetadata.gasLimit = gasLimit; + emit GasLimitUpdated(chainSelector, gasLimit); + } + + function bridgeToken( + uint64 destinationChainSelector, + address recipient, + uint256 amount + ) public payable { + bridgeToken(destinationChainSelector, recipient, amount, ""); + } + + /** + * @notice Bridge GYD from the current chain to the given chain * @param recipient The recipient of the bridged token * @param amount GYD amount */ - function bridgeToken(address recipient, uint256 amount, bytes memory data) - public - payable - virtual - { + function bridgeToken( + uint64 destinationChainSelector, + address recipient, + uint256 amount, + bytes memory data + ) public payable virtual { + ChainMetadata memory metadata = chainsMetadata[destinationChainSelector]; + if (metadata.targetAddress == address(0)) { + revert ChainNotSupported(destinationChainSelector); + } + _burn(msg.sender, amount); Client.EVM2AnyMessage memory evm2AnyMessage = CCIPHelpers.buildCCIPMessage( - destAddress, recipient, amount, data, bridgeGasLimit + metadata.targetAddress, recipient, amount, data, metadata.gasLimit ); - uint256 fees = router.getFee(mainnetChainSelector, evm2AnyMessage); + uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage); CCIPHelpers.sendCCIPMessage( - router, mainnetChainSelector, evm2AnyMessage, fees + router, destinationChainSelector, evm2AnyMessage, fees ); - emit GYDBridged(msg.sender, amount, totalSupply()); + emit GYDBridged( + destinationChainSelector, msg.sender, amount, totalSupply() + ); } - function getFee(address recipient, uint256 amount) - external - view - returns (uint256) - { - return getFee(recipient, amount, ""); + function getFee( + uint64 destinationChainSelector, + address recipient, + uint256 amount + ) external view returns (uint256) { + return getFee(destinationChainSelector, recipient, amount, ""); } - function getFee(address recipient, uint256 amount, bytes memory data) - public - view - returns (uint256) - { + function getFee( + uint64 destinationChainSelector, + address recipient, + uint256 amount, + bytes memory data + ) public view returns (uint256) { + ChainMetadata memory chainMeta = chainsMetadata[destinationChainSelector]; + if (chainMeta.targetAddress == address(0)) { + revert ChainNotSupported(destinationChainSelector); + } Client.EVM2AnyMessage memory evm2AnyMessage = CCIPHelpers.buildCCIPMessage( - destAddress, recipient, amount, data, bridgeGasLimit + chainMeta.targetAddress, recipient, amount, data, chainMeta.gasLimit ); - return router.getFee(mainnetChainSelector, evm2AnyMessage); - } - - /** - * Updates the gas limit when bridging to a chain - * @param gasLimit the new gas limit for this chain - */ - function updateGasLimit(uint256 gasLimit) external onlyOwner { - bridgeGasLimit = gasLimit; - emit GasLimitUpdated(gasLimit); + return router.getFee(destinationChainSelector, evm2AnyMessage); } /// @dev handle a received message @@ -161,11 +175,10 @@ contract L2Gyd is internal override { - if (any2EvmMessage.sourceChainSelector != mainnetChainSelector) { - revert MessageInvalid(); - } + ChainMetadata memory chainMeta = + chainsMetadata[any2EvmMessage.sourceChainSelector]; address actualSender = abi.decode(any2EvmMessage.sender, (address)); - if (actualSender != destAddress) { + if (actualSender != chainMeta.targetAddress) { revert MessageInvalid(); } @@ -176,6 +189,8 @@ contract L2Gyd is recipient.functionCall(data); } - emit GYDClaimed(recipient, amount, totalSupply()); + emit GYDClaimed( + any2EvmMessage.sourceChainSelector, recipient, amount, totalSupply() + ); } } diff --git a/test/GydL1Escrow.t.sol b/test/GydL1Escrow.t.sol index e6badb8..f65e59f 100644 --- a/test/GydL1Escrow.t.sol +++ b/test/GydL1Escrow.t.sol @@ -13,6 +13,7 @@ import {UUPSProxy} from "./UUPSProxy.sol"; import {Client} from "ccip/libraries/Client.sol"; import {CCIPReceiverUpgradeable} from "../src/CCIPReceiverUpgradeable.sol"; +import {IGydBridge} from "../src/IGydBridge.sol"; /** * @title GydL1EscrowV2Mock @@ -224,16 +225,14 @@ contract GydL1EscrowTest is Test { // Valid caller; invalid origin address vm.startPrank(routerAddress); - vm.expectRevert( - abi.encodeWithSelector(GydL1CCIPEscrow.MessageInvalid.selector) - ); + vm.expectRevert(abi.encodeWithSelector(IGydBridge.MessageInvalid.selector)); proxyV1.ccipReceive(_receivedMessage(chainSelector, address(0), data)); vm.stopPrank(); // Valid caller; invalid origin network vm.startPrank(routerAddress); vm.expectRevert( - abi.encodeWithSelector(GydL1CCIPEscrow.ChainNotSupported.selector, 0) + abi.encodeWithSelector(IGydBridge.ChainNotSupported.selector, 0) ); proxyV1.ccipReceive(_receivedMessage(0, originAddress, data)); vm.stopPrank(); diff --git a/test/L2Gyd.t.sol b/test/L2Gyd.t.sol index 572c0e2..87ac7a2 100644 --- a/test/L2Gyd.t.sol +++ b/test/L2Gyd.t.sol @@ -11,6 +11,7 @@ import {RouterMock} from "./RouterMock.sol"; import {Client} from "ccip/libraries/Client.sol"; import {CCIPReceiverUpgradeable} from "../src/CCIPReceiverUpgradeable.sol"; +import {IGydBridge} from "../src/IGydBridge.sol"; /** * @title L2GydV2Mock @@ -62,29 +63,21 @@ contract L2GydTest is Test { vm.createSelectFork(ARBITRUM_RPC_URL, 209_934_488); v1 = new L2Gyd(); - bytes memory v1Data = abi.encodeWithSelector( - L2Gyd.initialize.selector, - owner, - routerAddress, - destAddress, - mainnetChainSelector, - gasLimit - ); + bytes memory v1Data = + abi.encodeWithSelector(L2Gyd.initialize.selector, owner, routerAddress); UUPSProxy proxy = new UUPSProxy(address(v1), v1Data); proxyV1 = L2Gyd(address(proxy)); + vm.prank(owner); + proxyV1.addChain(mainnetChainSelector, destAddress, gasLimit); mockedV1 = new L2Gyd(); router = new RouterMock(); - bytes memory v2Data = abi.encodeWithSelector( - L2Gyd.initialize.selector, - owner, - address(router), - destAddress, - mainnetChainSelector, - gasLimit - ); + bytes memory v2Data = + abi.encodeWithSelector(L2Gyd.initialize.selector, owner, address(router)); UUPSProxy mockedProxy = new UUPSProxy(address(v1), v2Data); mockedProxyV1 = L2Gyd(address(mockedProxy)); + vm.prank(owner); + mockedProxyV1.addChain(mainnetChainSelector, destAddress, gasLimit); v2 = new L2GydV2Mock(); proxyV2 = L2GydV2Mock(address(proxyV1)); @@ -144,9 +137,12 @@ contract L2GydTest is Test { vm.stopPrank(); vm.startPrank(alice); - uint256 fees = mockedProxyV1.getFee(alice, bridgeAmount); + uint256 fees = + mockedProxyV1.getFee(mainnetChainSelector, alice, bridgeAmount); deal(alice, fees); - mockedProxyV1.bridgeToken{value: fees}(alice, bridgeAmount); + mockedProxyV1.bridgeToken{value: fees}( + mainnetChainSelector, alice, bridgeAmount + ); vm.stopPrank(); assertEq(mockedProxyV1.balanceOf(alice), 0); @@ -173,9 +169,9 @@ contract L2GydTest is Test { vm.stopPrank(); vm.startPrank(alice); - uint256 fees = proxyV1.getFee(alice, bridgeAmount); + uint256 fees = proxyV1.getFee(mainnetChainSelector, alice, bridgeAmount); deal(alice, fees); - proxyV1.bridgeToken{value: fees}(alice, bridgeAmount); + proxyV1.bridgeToken{value: fees}(mainnetChainSelector, alice, bridgeAmount); vm.stopPrank(); assertEq(proxyV1.balanceOf(alice), 0); @@ -205,14 +201,14 @@ contract L2GydTest is Test { vm.stopPrank(); vm.startPrank(alice); - uint256 fees = proxyV1.getFee(alice, bridgeAmount); + uint256 fees = proxyV1.getFee(mainnetChainSelector, alice, bridgeAmount); deal(alice, fees); - proxyV1.bridgeToken{value: fees}(alice, bridgeAmount); + proxyV1.bridgeToken{value: fees}(mainnetChainSelector, alice, bridgeAmount); vm.stopPrank(); address currentRouterAddress = address(proxyV1.router()); - address originAddress = proxyV1.destAddress(); - uint64 chainSelector = proxyV1.mainnetChainSelector(); + (address originAddress,) = proxyV1.chainsMetadata(mainnetChainSelector); + uint64 chainSelector = mainnetChainSelector; bytes memory metadata = abi.encode(bob, 1 ether, ""); // Invalid caller @@ -229,13 +225,13 @@ contract L2GydTest is Test { // Valid caller; invalid origin address vm.startPrank(currentRouterAddress); - vm.expectRevert(abi.encodeWithSelector(L2Gyd.MessageInvalid.selector)); + vm.expectRevert(abi.encodeWithSelector(IGydBridge.MessageInvalid.selector)); proxyV1.ccipReceive(_receivedMessage(chainSelector, address(0), metadata)); vm.stopPrank(); // Valid caller; invalid origin network vm.startPrank(currentRouterAddress); - vm.expectRevert(abi.encodeWithSelector(L2Gyd.MessageInvalid.selector)); + vm.expectRevert(abi.encodeWithSelector(IGydBridge.MessageInvalid.selector)); proxyV1.ccipReceive(_receivedMessage(1, originAddress, metadata)); vm.stopPrank(); @@ -260,14 +256,14 @@ contract L2GydTest is Test { vm.stopPrank(); vm.startPrank(alice); - uint256 fees = proxyV1.getFee(alice, bridgeAmount); + uint256 fees = proxyV1.getFee(mainnetChainSelector, alice, bridgeAmount); deal(alice, fees); - proxyV1.bridgeToken{value: fees}(bob, bridgeAmount); + proxyV1.bridgeToken{value: fees}(mainnetChainSelector, bob, bridgeAmount); vm.stopPrank(); address currentRouterAddress = address(proxyV1.router()); - address originAddress = proxyV1.destAddress(); - uint64 chainSelector = proxyV1.mainnetChainSelector(); + (address originAddress,) = proxyV1.chainsMetadata(mainnetChainSelector); + uint64 chainSelector = mainnetChainSelector; bytes memory messageData = abi.encode(bob, bridgeAmount, ""); vm.startPrank(currentRouterAddress); @@ -284,9 +280,10 @@ contract L2GydTest is Test { uint256 newGasLimit = 100_000; vm.prank(owner); - proxyV2.updateGasLimit(newGasLimit); + proxyV2.updateGasLimit(mainnetChainSelector, newGasLimit); + (, uint256 gasLimit_) = proxyV2.chainsMetadata(mainnetChainSelector); - assertEq(proxyV2.bridgeGasLimit(), newGasLimit); + assertEq(gasLimit_, newGasLimit); } function _receivedMessage( From a3aa3baa4178c5339091e3e36f56e60f5aabd29f Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Fri, 13 Sep 2024 18:11:30 +0100 Subject: [PATCH 2/2] Use single value for bridged GYD --- src/GydL1CCIPEscrow.sol | 25 ++++++++++++++++++++----- test/GydL1Escrow.t.sol | 14 ++++++-------- test/GydL1EscrowUpgrade.t.sol | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 test/GydL1EscrowUpgrade.t.sol diff --git a/src/GydL1CCIPEscrow.sol b/src/GydL1CCIPEscrow.sol index 55a6d82..7da36bc 100644 --- a/src/GydL1CCIPEscrow.sol +++ b/src/GydL1CCIPEscrow.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import {Initializable} from "upgradeable/proxy/utils/Initializable.sol"; +import {StorageSlot} from "oz/utils/StorageSlot.sol"; import {UUPSUpgradeable} from "upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {AccessControlDefaultAdminRulesUpgradeable} from "upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; @@ -30,6 +31,12 @@ contract GydL1CCIPEscrow is using Address for address; using Address for address payable; + // Previously stored in a mapping(uint64 => uint256) at slot 4 where the + // uint64 is the CCIP chain selector + // only Arbitrum was used, so we compute its slot + bytes32 private constant _PREVIOUS_TOTAL_BRIDGED_SLOT = + keccak256(abi.encode(4_949_039_107_694_359_620, 4)); + /// @notice GYD contract IERC20 public gyd; @@ -42,7 +49,7 @@ contract GydL1CCIPEscrow is mapping(uint64 => ChainMetadata) public chainsMetadata; /// @notice The total amount of GYD bridged per chain - mapping(uint64 => uint256) public totalBridgedGYD; + uint256 public totalBridgedGYD; /// @notice Disable initializer on deploy constructor() { @@ -77,6 +84,14 @@ contract GydL1CCIPEscrow is } } + function initializeTotalBridgedGYD() external { + if (totalBridgedGYD > 0) { + revert InvalidInitialization(); + } + totalBridgedGYD = + StorageSlot.getUint256Slot(_PREVIOUS_TOTAL_BRIDGED_SLOT).value; + } + /** * @dev The GydL1Escrow can only be upgraded by the owner * @param v new GydL1Escrow implementation @@ -150,9 +165,9 @@ contract GydL1CCIPEscrow is router, destinationChainSelector, evm2AnyMessage, fees ); - uint256 bridged = totalBridgedGYD[destinationChainSelector]; + uint256 bridged = totalBridgedGYD; bridged += amount; - totalBridgedGYD[destinationChainSelector] = bridged; + totalBridgedGYD = bridged; emit GYDBridged(destinationChainSelector, msg.sender, amount, bridged); } @@ -210,9 +225,9 @@ contract GydL1CCIPEscrow is (address recipient, uint256 amount, bytes memory data) = abi.decode(any2EvmMessage.data, (address, uint256, bytes)); - uint256 bridged = totalBridgedGYD[any2EvmMessage.sourceChainSelector]; + uint256 bridged = totalBridgedGYD; bridged -= amount; - totalBridgedGYD[any2EvmMessage.sourceChainSelector] = bridged; + totalBridgedGYD = bridged; gyd.safeTransfer(recipient, amount); if (data.length > 0) { diff --git a/test/GydL1Escrow.t.sol b/test/GydL1Escrow.t.sol index f65e59f..cbe9e84 100644 --- a/test/GydL1Escrow.t.sol +++ b/test/GydL1Escrow.t.sol @@ -28,7 +28,7 @@ contract GydL1EscrowV2Mock is GydL1CCIPEscrow { virtual override { - totalBridgedGYD[0] = 2 * amount; + totalBridgedGYD = 2 * amount; } } @@ -110,14 +110,14 @@ contract GydL1EscrowTest is Test { /// @notice Upgrade as admin; make sure it works as expected function testUpgradeAsAdmin() public { // Pre-upgrade check - assertEq(proxyV1.totalBridgedGYD(0), 0); + assertEq(proxyV1.totalBridgedGYD(), 0); vm.expectRevert("ERC20: insufficient allowance"); proxyV1.bridgeToken(0, alice, 1 ether); vm.startPrank(admin); proxyV1.upgradeToAndCall(address(v2), ""); proxyV2.bridgeToken(0, alice, 1 ether); - assertEq(proxyV1.totalBridgedGYD(0), 2 ether); + assertEq(proxyV1.totalBridgedGYD(), 2 ether); } /// @notice Upgrade as non-admin; make sure it reverted @@ -154,9 +154,7 @@ contract GydL1EscrowTest is Test { assertEq(IERC20(gyd).balanceOf(alice), 0); assertEq(IERC20(gyd).balanceOf(address(mockedProxyV1)), bridgeAmount); - assertEq( - mockedProxyV1.totalBridgedGYD(arbitrumChainSelector), bridgeAmount - ); + assertEq(mockedProxyV1.totalBridgedGYD(), bridgeAmount); assertEq(router.destinationChainSelector(), arbitrumChainSelector); assertEq(router.destAddress(), gyd); @@ -181,7 +179,7 @@ contract GydL1EscrowTest is Test { ); vm.stopPrank(); - assertEq(proxyV1.totalBridgedGYD(arbitrumChainSelector), bridgeAmount); + assertEq(proxyV1.totalBridgedGYD(), bridgeAmount); assertEq(IERC20(gyd).balanceOf(alice), 0); assertEq(IERC20(gyd).balanceOf(address(proxyV1)), bridgeAmount); } @@ -269,7 +267,7 @@ contract GydL1EscrowTest is Test { vm.stopPrank(); assertEq(IERC20(gyd).balanceOf(bob), bridgeAmount); - assertEq(proxyV1.totalBridgedGYD(arbitrumChainSelector), 0); + assertEq(proxyV1.totalBridgedGYD(), 0); } function testUpdateGasLimit() public { diff --git a/test/GydL1EscrowUpgrade.t.sol b/test/GydL1EscrowUpgrade.t.sol new file mode 100644 index 0000000..7fdc3d7 --- /dev/null +++ b/test/GydL1EscrowUpgrade.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +import {GydL1CCIPEscrow} from "../src/GydL1CCIPEscrow.sol"; + +contract GydL1EscrowUpgradeTest is Test { + GydL1CCIPEscrow escrow = + GydL1CCIPEscrow(0xa1886c8d748DeB3774225593a70c79454B1DA8a6); + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20_743_048); + } + + function testUpgrade() external { + address newImpl = address(new GydL1CCIPEscrow()); + bytes memory data = abi.encodeWithSelector( + GydL1CCIPEscrow.initializeTotalBridgedGYD.selector + ); + + vm.prank(escrow.owner()); + escrow.upgradeToAndCall(newImpl, data); + + assertGt(escrow.totalBridgedGYD(), 0); + assertLt(escrow.totalBridgedGYD(), 30_000_000e18); + } +}