Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/share ccip bridge #180

Merged
merged 12 commits into from
Jan 23, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
cache/
out/
broadcast/
gnosisTxs/

# Environment variables!
.env
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@
[submodule "lib/pendle-core-v2-public"]
path = lib/pendle-core-v2-public
url = https://github.com/pendle-finance/pendle-core-v2-public
[submodule "lib/ccip"]
path = lib/ccip
url = https://github.com/smartcontractkit/ccip
1 change: 1 addition & 0 deletions lib/ccip
Submodule ccip added at c8eed8
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/
@chainlink/=lib/chainlink/
@uniswapV3P=lib/v3-periphery/contracts/
@uniswapV3C=lib/v3-core/contracts/
@balancer=lib/balancer-v2-monorepo/pkg
@balancer=lib/balancer-v2-monorepo/pkg
@ccip=lib/ccip/
49 changes: 49 additions & 0 deletions src/mocks/MockCCIPRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";

contract MockCCIPRouter {
ERC20 public immutable LINK;

constructor(address _link) {
LINK = ERC20(_link);
}

uint256 public messageCount;

uint256 public currentFee = 1e18;

uint64 public constant SOURCE_SELECTOR = 6101244977088475029;
uint64 public constant DESTINATION_SELECTOR = 16015286601757825753;

mapping(bytes32 => Client.Any2EVMMessage) public messages;

bytes32 public lastMessageId;

function setFee(uint256 newFee) external {
currentFee = newFee;
}

function getLastMessage() external view returns (Client.Any2EVMMessage memory) {
return messages[lastMessageId];
}

function getFee(uint64, Client.EVM2AnyMessage memory) external view returns (uint256) {
return currentFee;
}

function ccipSend(uint64 chainSelector, Client.EVM2AnyMessage memory message) external returns (bytes32 messageId) {
LINK.transferFrom(msg.sender, address(this), currentFee);
messageId = bytes32(messageCount);
messageCount++;
lastMessageId = messageId;
messages[messageId].messageId = messageId;
messages[messageId].sourceChainSelector = chainSelector == SOURCE_SELECTOR
? DESTINATION_SELECTOR
: SOURCE_SELECTOR;
messages[messageId].sender = abi.encode(msg.sender);
messages[messageId].data = message.data;
}
}
169 changes: 169 additions & 0 deletions src/modules/multi-chain-share/DestinationMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { CCIPReceiver } from "@ccip/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import { Client } from "@ccip/contracts/src/v0.8/ccip/libraries/Client.sol";
import { IRouterClient } from "@ccip/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol";

/**
* @title DestinationMinter
* @notice Receives CCIP messages from SourceLocker, to mint ERC20 shares that
* represent ERC4626 shares locked on source chain.
* @author crispymangoes
*/
contract DestinationMinter is ERC20, CCIPReceiver {
using SafeTransferLib for ERC20;

//============================== ERRORS ===============================

error DestinationMinter___SourceChainNotAllowlisted(uint64 sourceChainSelector);
error DestinationMinter___SenderNotAllowlisted(address sender);
error DestinationMinter___InvalidTo();
0xEinCodes marked this conversation as resolved.
Show resolved Hide resolved
error DestinationMinter___FeeTooHigh();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this error, did we want to outline the amount of fees that was erroneous? Or is it just self-explanatory since they are user input params again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this error only bubbles up if the user specified maxLinkToPay is less than the fees it would cost to send the bridge TX. So we could probably add in 2 args, the user specified max, and the actual fees being charged. However I dont think etherscan knows how to process custom errors, so this would only help people interacting with our contracts via some dev env that tells them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that Etherscan is limited in that sense. So how would this pop up on etherscan if it were to revert because of this? If nothing can be done about it so it helps users, then that is fine to leave this unchanged.


//============================== EVENTS ===============================

event BridgeToSource(uint256 amount, address to);
event BridgeFromSource(uint256 amount, address to);

//============================== MODIFIERS ===============================

modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
if (_sourceChainSelector != sourceChainSelector)
revert DestinationMinter___SourceChainNotAllowlisted(_sourceChainSelector);
if (_sender != targetSource) revert DestinationMinter___SenderNotAllowlisted(_sender);
_;
}

//============================== IMMUTABLES ===============================

/**
* @notice The address of the SourceLocker on source chain.
*/
address public immutable targetSource;

/**
* @notice The CCIP source chain selector.
*/
uint64 public immutable sourceChainSelector;

/**
* @notice The CCIP destination chain selector.
*/
uint64 public immutable destinationChainSelector;

/**
* @notice This networks LINK contract.
*/
ERC20 public immutable LINK;

/**
* @notice The message gas limit to use for CCIP messages.
*/
uint256 public immutable messageGasLimit;

constructor(
address _router,
address _targetSource,
string memory _name,
string memory _symbol,
uint8 _decimals,
uint64 _sourceChainSelector,
uint64 _destinationChainSelector,
address _link,
uint256 _messageGasLimit
) ERC20(_name, _symbol, _decimals) CCIPReceiver(_router) {
targetSource = _targetSource;
sourceChainSelector = _sourceChainSelector;
destinationChainSelector = _destinationChainSelector;
LINK = ERC20(_link);
messageGasLimit = _messageGasLimit;
}

//============================== BRIDGE ===============================

/**
* @notice Bridge shares back to source chain.
* @dev Caller should approve LINK to be spent by this contract.
* @param amount Number of shares to burn on destination network and unlock/transfer on source network.
* @param to Specified address to burn destination network `share` tokens, and receive unlocked `share` tokens on source network.
* @param maxLinkToPay Specified max amount of LINK fees to pay as per this contract.
* @return messageId Resultant CCIP messageId.
*/
function bridgeToSource(uint256 amount, address to, uint256 maxLinkToPay) external returns (bytes32 messageId) {
if (to == address(0)) revert DestinationMinter___InvalidTo();
_burn(msg.sender, amount);

Client.EVM2AnyMessage memory message = _buildMessage(amount, to);

IRouterClient router = IRouterClient(this.getRouter());

uint256 fees = router.getFee(sourceChainSelector, message);

if (fees > maxLinkToPay) revert DestinationMinter___FeeTooHigh();

LINK.safeTransferFrom(msg.sender, address(this), fees);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caller will have to approve LINK to be sent to this DestinationMinter, add note in the natspec?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, do we want to have a revokeApprovals at the end of this function for LINK?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm that is a good question. The router address is verified to be safe, and these contracts will not be holding any customer funds that are LINK. So it would probably only help us if for some reason the router did not transfer the link it said it would, and the LINK contract had ERC20 breaking logic like USDT where we revert when going from non zero allowance to non zero allowance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but since we know that is not the case with the LINK contract, are you saying that we do not need to have a revokeApprovals applied here?


LINK.safeApprove(address(router), fees);

messageId = router.ccipSend(sourceChainSelector, message);
emit BridgeToSource(amount, to);
}

//============================== VIEW FUNCTIONS ===============================

/**
* @notice Preview fee required to bridge shares back to source.
* @param amount Specified amount of `share` tokens to bridge to source network.
* @param to Specified address to receive bridged shares on source network.
* @return fee required to bridge shares.
*/
function previewFee(uint256 amount, address to) public view returns (uint256 fee) {
Client.EVM2AnyMessage memory message = _buildMessage(amount, to);

IRouterClient router = IRouterClient(this.getRouter());

fee = router.getFee(sourceChainSelector, message);
}

//============================== CCIP RECEIVER ===============================

/**
* @notice Implement internal _ccipRecevie function logic.
* @param any2EvmMessage CCIP encoded message specifying details to use to 'mint' `share` tokens to a specified address `to` on destination network.
*/
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
)
internal
override
onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)))
{
(uint256 amount, address to) = abi.decode(any2EvmMessage.data, (uint256, address));
_mint(to, amount);
emit BridgeFromSource(amount, to);
}

//============================== INTERNAL HELPER ===============================

/**
* @notice Build the CCIP message to send to source locker.
* @param amount number of `share` token to bridge.
* @param to Specified address to receive unlocked bridged shares on source network.
* @return message the CCIP message to send to source locker.
*/
function _buildMessage(uint256 amount, address to) internal view returns (Client.EVM2AnyMessage memory message) {
message = Client.EVM2AnyMessage({
receiver: abi.encode(targetSource),
data: abi.encode(amount, to),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit and non-strict sequencing mode
Client.EVMExtraArgsV1({ gasLimit: messageGasLimit /*, strict: false*/ })
),
feeToken: address(LINK)
});
}
}
Loading
Loading