-
Notifications
You must be signed in to change notification settings - Fork 96
Cross-chain Strategy #2715
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
base: master
Are you sure you want to change the base?
Cross-chain Strategy #2715
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2715 +/- ##
==========================================
+ Coverage 39.40% 43.65% +4.25%
==========================================
Files 126 133 +7
Lines 5789 6132 +343
Branches 1537 1634 +97
==========================================
+ Hits 2281 2677 +396
+ Misses 3506 3452 -54
- Partials 2 3 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| pragma solidity ^0.8.0; | ||
|
|
||
| /** | ||
| * @title OUSD Yearn V3 Remote Strategy Mock - the Mainnet part |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"- the Mainnet part" should probably be "- the L2 chain part"
contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol
Outdated
Show resolved
Hide resolved
| address public immutable cctpHookWrapper; | ||
|
|
||
| // USDC address on local chain | ||
| address public immutable baseToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can rename this to usdcToken?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Since we would only be dealing with USDC with CCTP
contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol
Outdated
Show resolved
Hide resolved
* fix deploy files * minor rename * add calls to Morpho Vault into a try catch * refactor hook wrapper * don't revert if withdraw from underlying fails * use checkBalance for deposit/withdrawal acknowledgment * update message in remote strategy * remove unneeded functions
naddison36
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my first pass of the contracts
| using SafeERC20 for IERC20; | ||
| using CrossChainStrategyHelper for bytes; | ||
|
|
||
| // Remote strategy balance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: lets make this Natspec with /// @notice
| // Remote strategy balance | ||
| uint256 public remoteStrategyBalance; | ||
|
|
||
| // Amount that's bridged but not yet received on the destination chain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can be Natspec with /// @notice
| // USDC address on local chain | ||
| address public immutable baseToken; | ||
|
|
||
| // Domain ID of the chain from which messages are accepted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: best to make all these comments on public variables a Natspec with /// @notice
| // Domain ID of the chain from which messages are accepted | ||
| uint32 public immutable peerDomainID; | ||
|
|
||
| // Strategy address on other chain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd add the example values. eg Ethereum 0, Base 6
| // Strategy address on other chain | ||
| address public immutable peerStrategy; | ||
|
|
||
| // CCTP params |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: a link to the CCTP
Finality thresholds would be good here https://developers.circle.com/cctp/technical-guide#finality-thresholds
| } | ||
| } | ||
|
|
||
| /// @inheritdoc InitializableAbstractStrategy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think its better to have CrossChainMasterStrategy specific Natspec. eg _recipient must be the vault and _asset must be USDC.
| */ | ||
| function _deposit(address _asset, uint256 depositAmount) internal virtual { | ||
| require(_asset == baseToken, "Unsupported asset"); | ||
| require(!isTransferPending(), "Transfer already pending"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it feels safer to have this check in _getNextNonce
| require(_asset == baseToken, "Unsupported asset"); | ||
| require(!isTransferPending(), "Transfer already pending"); | ||
| require(pendingAmount == 0, "Unexpected pending amount"); | ||
| require(depositAmount > 0, "Deposit amount must be greater than 0"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can be shortened to Must deposit something which is what a lot of other strategies use
| require(depositAmount > 0, "Deposit amount must be greater than 0"); | ||
| require( | ||
| depositAmount <= MAX_TRANSFER_AMOUNT, | ||
| "Deposit amount exceeds max transfer amount" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can be shortened to Deposit amount exceeds max so it fits in an EVM word
|
|
||
| /** | ||
| * @notice Set address of Strategist. | ||
| * This is important to have the function name same as IVault. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this saying the Strategist needs to be the vaultAddress?
The constructor states
// Vault address must always be the proxy address
// so that IVault(vaultAddress).strategistAddr() works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I think I could've worded this better, I'll change it.
So, the problem is that the Generalized4626Strategy uses IVault.strategistAddr(). However the remote strategy has no Vault, it only communicates with the Master strategy. So, whenever IVault.strategistAddr() is used in the abstract strategy, it'll revert. So, what I did is add a function with same signature as strategistAddr(), so whenever IVault.strategistAddr() is called, it'll be same as calling address(this).strategistAddr().
Strategist should always be the 2/8 multisig. But for Remote strategy, the Vault should be its own proxy addresss
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see now. So the CrossChainRemoteStrategy constructor should check
_baseConfig.vaultAddress == address(this)
clement-ux
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First surface pass
| // Operator address: Can relay CCTP messages | ||
| address public operator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Global comment about slot arrangement, maybe we can:
- Move
MAX_TRANSFER_AMOUNTabovecctpMessageTransmitterto keep order: constant -> immutable -> variable - cast
minFinalityThresholdandfeePremiumBpsintouint16. - move
nonceProcessedbellowoperator - pack
minFinalityThreshold,feePremiumBpsandlastTransferNonceall together in a single slot.
| require( | ||
| minFinalityThreshold == 1000, | ||
| "Unfinalized messages are not supported" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick question, what's the reason for only supporting fast transfer and not standard transfer?
| // CCTP Message Header fields | ||
| // Ref: https://developers.circle.com/cctp/technical-guide#message-header | ||
| uint8 constant VERSION_INDEX = 0; | ||
| uint8 constant SOURCE_DOMAIN_INDEX = 4; | ||
| uint8 constant SENDER_INDEX = 44; | ||
| uint8 constant RECIPIENT_INDEX = 76; | ||
| uint8 constant MESSAGE_BODY_INDEX = 148; | ||
|
|
||
| // Message body V2 fields | ||
| // Ref: https://developers.circle.com/cctp/technical-guide#message-body | ||
| // Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol | ||
| uint8 constant BURN_MESSAGE_V2_VERSION_INDEX = 0; | ||
| uint8 constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; | ||
| uint8 constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; | ||
| uint8 constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; | ||
| uint8 constant BURN_MESSAGE_V2_FEE_EXECUTED_INDEX = 164; | ||
| uint8 constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Shouldn't we create a Library for these constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And maybe we could move this function into the library too:
origin-dollar/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol
Lines 452 to 479 in b2feed1
| /** | |
| * @dev Decodes the CCTP message header | |
| * @param message Message to decode | |
| * @return version Version of the message | |
| * @return sourceDomainID Source domain ID | |
| * @return sender Sender of the message | |
| * @return recipient Recipient of the message | |
| * @return messageBody Message body | |
| */ | |
| function _decodeMessageHeader(bytes memory message) | |
| internal | |
| pure | |
| returns ( | |
| uint32 version, | |
| uint32 sourceDomainID, | |
| address sender, | |
| address recipient, | |
| bytes memory messageBody | |
| ) | |
| { | |
| version = message.extractUint32(VERSION_INDEX); | |
| sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); | |
| // Address of MessageTransmitterV2 caller on source domain | |
| sender = message.extractAddress(SENDER_INDEX); | |
| // Address to handle message body on destination domain | |
| recipient = message.extractAddress(RECIPIENT_INDEX); | |
| messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); | |
| } |
| uint256 undepositedUSDC = IERC20(baseToken).balanceOf(address(this)); | ||
| return undepositedUSDC + pendingAmount + remoteStrategyBalance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I don't think it is necessary to cache undepositedUSDC, we can directly have:
return IERC20(baseToken).balanceOf(address(this)) + pendingAmount + remoteStrategyBalance;| _markNonceAsProcessed(nonce); | ||
|
|
||
| // Effect of confirming a deposit, reset pending amount | ||
| pendingAmount = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gas: delete pendingAmount should be cheaper than pendingAmount = 0;
| /// @inheritdoc Generalized4626Strategy | ||
| function withdrawAll() external virtual override onlyGovernorOrStrategist { | ||
| uint256 contractBalance = IERC20(baseToken).balanceOf(address(this)); | ||
| uint256 balance = checkBalance(baseToken) - contractBalance; | ||
| _withdraw(address(this), baseToken, balance); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It fetches IERC20(baseToken).balanceOf(address(this)) two times, one at line 139, once again in checkBalance.
Maybe we can have something like this:
function withdrawAll() external virtual override onlyGovernorOrStrategist {
_withdraw(address(this), baseToken, IERC4626(platformAddress).previewRedeem(platform.balanceOf(address(this))));
}| // Remote strategy balance | ||
| uint256 public remoteStrategyBalance; | ||
|
|
||
| // Amount that's bridged but not yet received on the destination chain | ||
| uint256 public pendingAmount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can downcast them to uint128 or uint64, because USDC has only 6 decimals, and then use just one slot.
However, this is maybe not worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The max value that can be here is 10M USDC (10M * 10**6), so yeah it can fit in uint64, let me think about it. Because it might cause problems, if we ever decide to use this strategy for OETH
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly, that's why it could be not worth it. Choose the solution you prefer.
clement-ux
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding some comments
| function _onMessageReceived(bytes memory payload) internal override { | ||
| uint32 messageType = payload.getMessageType(); | ||
| if (messageType == CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE) { | ||
| // Received when Remote strategy checks the balance | ||
| _processBalanceCheckMessage(payload); | ||
| } else { | ||
| revert("Unknown message type"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can have something lighter and a bit more gas friendly:
function _onMessageReceived(bytes memory payload) internal override {
require(
payload.getMessageType() ==
CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE,
"Unknown message type"
);
_processBalanceCheckMessage(payload);
}| require(_asset == baseToken, "Unsupported asset"); | ||
| require(_amount > 0, "Withdraw amount must be greater than 0"); | ||
| require(_recipient == vaultAddress, "Only Vault can withdraw"); | ||
| require(!isTransferPending(), "Transfer already pending"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that checking if there is a "Transfer already pending" is checked two times:
require(!isTransferPending(), "Transfer already pending");_getNextNonce()
I agree that having two times the check is not bad, but this means that we could probably lighten the code somewhere.
Should it be on the _getNextNonce()or on the _deposit()/_withdraw(), I let you be the judge.
Code Changes
Check readme: https://github.com/OriginProtocol/origin-dollar/blob/shah/cross-chain-strategy-cctpv2/contracts/contracts/strategies/crosschain/crosschain-strategy.md