diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b12e5f --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Auxo X Chain + + + Warning! This repository is incomplete state and will be changing heavily + + +This repository contains the source code for the Auxo Cross chain hub, it consists of 3 parts: + +1. The XChainHub - an interface for interacting with vaults and strategies deployed on multiple chains. + +2. The XChainStrategy - an implementation of the same BaseStrategy used in the Auxo Vaults that adds support for using the XChainHub. + +3. LayerZeroApp - an implementation of a nonBlockingLayerZero application that allows for cross chain messaging using the LayerZero protocol. + + +## The Hub +---------- +The hub itself allows users to intiate vault actions from any chain and have them be executed on any other chain with a [LayerZero endpoint](https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids). + +LayerZero applications initiate cross chain requests by calling the `endpoint.send` method, which then invokes `_nonBlockingLzReceive` on the destination chain. + +The hub itself implements a reducer pattern to route inbound messages from the `_nonBlockingLzReceive` function, to a particular internal action. The main actions are: + +1. Deposit into a vault. +2. Request to withdraw from a vault (this begins the batch burn process). +3. Provided we have a successful batch burn, complete a batch burn request and send underlying funds from a vault to a user. +4. Report changes in the underlying balance of each strategy on a given chain. + +Therefore, each cross chain request will go through the following workflow: + +1. Call the Cross Chain function on the source chain (i.e. `depositToChain`) +2. Cross chain function will call the `_lzSend` method, which in turn calls the `LayerZeroEndpoint.send` method. +3. The LZ endpoint will call `_nonBlockingLzReceive` on the destination chain. +4. The reducer at `_nonBlockingLzReceive` will call the corresponding action passed in the payload (i.e. `_depositAction`) + +## Swaps +---------- +Currently, the hub utilises the [Anyswap router](https://github.com/anyswap/CrossChain-Router/wiki/How-to-integrate-AnySwap-Router) to execute cross chain deposits of the underlying token into the auxo vault. We are discussing removing the router and replacing with stargate. + +### Advantages of Stargate: +- Guaranteed instant finality if the transaction is approved, due to Stargate's cross-chain liquidity pooling. +- We can pass our payload data to the Stargate router and use `sgReceive` to both handle the swapping, and post-swap logic. This would remove the need for calls to both the LayerZero endpoint and to Anyswap router. + + +### Advantages of Anyswap: +- Anyswap supports a much larger array of tokens, whereas stargate only implements a few stables and Eth. + + +## Implementing Stargate To Do List +-------- +*This is a development reference only* + +Our to do list to swap out AnySwap/LayerZero with Stargate is as follows: + +- [ ] Add the `IStargateRouter` to the list of interfaces +- [ ] Replace the AnySwap swaps with `IStargateRouter.swap` + - [ ] `_finalizeWithdrawAction` + - [ ] `constructor` + - [ ] `depositToChain` + +- [ ] Replace* the `_lzSend` messages with encoded payloads in `IStargateRouter.swap` + - [ ] `depositToChain` + - [ ] `reportUnderlying`* +- [ ] Replace the `_nonBlockingLzReceive` with `IStargateReceiver.sgReceive` + +> \*[IStargateRouter](https://stargateprotocol.gitbook.io/stargate/interfaces/evm-solidity-interfaces/istargaterouter.sol) *does not appear to have a way of sending just messages across (without swaps), it may be that we have to implement both LayerZero and stargate to get the cross chain reporting* \ No newline at end of file diff --git a/src/XChainHub.sol b/src/XChainHub.sol index f7270dd..023a1fa 100644 --- a/src/XChainHub.sol +++ b/src/XChainHub.sol @@ -27,26 +27,46 @@ import {IAnyswapRouter} from "./interfaces/IAnyswapRouter.sol"; contract XChainHub is LayerZeroApp { using SafeERC20 for IERC20; - /// Actions - + /*/////////////////////////////////////////////////////////////// + Action Enums + //////////////////////////////////////////////////////////////*/ + + /// Enter into a vault uint8 internal constant DEPOSIT_ACTION = 0; + /// Begin the batch burn process uint8 internal constant REQUEST_WITHDRAW_ACTION = 1; + /// Withdraw funds once batch burn completed uint8 internal constant FINALIZE_WITHDRAW_ACTION = 2; + /// Report underlying from different chain uint8 internal constant REPORT_UNDERLYING_ACTION = 3; - /// Report delay - uint64 internal constant REPORT_DELAY = 6 hours; - - /// Message struct + /*/////////////////////////////////////////////////////////////// + Structs + //////////////////////////////////////////////////////////////*/ + /// @notice Message struct + /// @param action is the number of the action above + /// @param payload is the encoded data to be sent with the message struct Message { uint8 action; bytes payload; } + /*/////////////////////////////////////////////////////////////// + Constants & Immutables + //////////////////////////////////////////////////////////////*/ + + /// Report delay + uint64 internal constant REPORT_DELAY = 6 hours; + /// @notice Anyswap router. + /// @dev to be replaced by Stargate Router IAnyswapRouter public immutable anyswapRouter; + /*/////////////////////////////////////////////////////////////// + Single Chain Mappings + //////////////////////////////////////////////////////////////*/ + /// @notice Trusted vaults on current chain. mapping(address => bool) public trustedVault; @@ -58,8 +78,13 @@ contract XChainHub is LayerZeroApp { mapping(address => bool) public exiting; /// @notice Indicates withdrawn amount per round for a given vault. + /// @dev format vaultAddr => round => withdrawn mapping(address => mapping(uint256 => uint256)) public withdrawnPerRound; + /*/////////////////////////////////////////////////////////////// + Cross Chain Mappings (chainId => strategy => X) + //////////////////////////////////////////////////////////////*/ + /// @notice Shares held on behalf of strategies from other chains. /// @dev This is for DESTINATION CHAIN. /// @dev Each strategy will have one and only one underlying forever. @@ -69,32 +94,65 @@ contract XChainHub is LayerZeroApp { /// the XChainHub on chain B will account for minted shares. mapping(uint16 => mapping(address => uint256)) public sharesPerStrategy; - /// @notice Current round per strategy. + /// @notice Destination Chain ID => Strategy => CurrentRound mapping(uint16 => mapping(address => uint256)) public currentRoundPerStrategy; /// @notice Shares waiting for social burn process. - /// @dev This is for DESTINATION CHAIN. + /// Destination Chain ID => Strategy => ExitingShares mapping(uint16 => mapping(address => uint256)) public exitingSharesPerStrategy; /// @notice Latest updates per strategy + /// Destination Chain ID => Strategy => LatestUpdate mapping(uint16 => mapping(address => uint256)) public latestUpdate; + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + /// @param anyswapEndpoint TO BE DEPRECATED + /// @dev will be replaced with stargate + /// @param lzEndpoint address of the layerZero endpoint contract on the source chain constructor(address anyswapEndpoint, address lzEndpoint) LayerZeroApp(lzEndpoint) { anyswapRouter = IAnyswapRouter(anyswapEndpoint); } + /*/////////////////////////////////////////////////////////////// + Single Chain Functions + //////////////////////////////////////////////////////////////*/ + + /// @notice updates a vault on the current chain to be either trusted or untrusted + /// @dev trust determines whether a vault can be interacted with + /// @dev This is callable only by the owner function setTrustedVault(address vault, bool trusted) external onlyOwner { trustedVault[vault] = trusted; } + /// @notice indicates whether the vault is in an `exiting` state + /// which restricts certain methods + /// @dev This is callable only by the owner function setExiting(address vault, bool exit) external onlyOwner { exiting[vault] = exit; } + /*/////////////////////////////////////////////////////////////// + Cross Chain Functions + //////////////////////////////////////////////////////////////*/ + + /// @notice iterates through a list of destination chains and sends the current value of + /// the strategy (in terms of the underlying vault token) to that chain. + /// @param vault Vault on the current chain. + /// @param dstChains is an array of the layerZero chain Ids to check + /// @param strats array of strategy addresses on the destination chains, index should match the dstChainId + /// @param adapterParams is additional info to send to the Lz receiver + /// @dev There are a few caveats: + /// 1. All strategies must have deposits. + /// 2. Requires that the setTrustedRemote method be set from lzApp, with the address being the deploy + /// address of this contract on the dstChain. + /// 3. The list of chain ids and strategy addresses must be the same length, and use the same underlying token. function reportUnderlying( IVault vault, uint16[] memory dstChains, @@ -113,6 +171,7 @@ contract XChainHub is LayerZeroApp { uint256 amountToReport; uint256 exchangeRate = vault.exchangeRate(); + for (uint256 i; i < dstChains.length; i++) { uint256 shares = sharesPerStrategy[dstChains[i]][strats[i]]; @@ -124,9 +183,14 @@ contract XChainHub is LayerZeroApp { "XChainHub: latest update too recent" ); + // record the latest update for future reference latestUpdate[dstChains[i]][strats[i]] = block.timestamp; + amountToReport = (shares * exchangeRate) / 10**vault.decimals(); + // See Layer zero docs for details on _lzSend + // Corrolary method is _nonblockingLzReceive which will be invoked + // on the destination chain _lzSend( dstChains[i], abi.encode(REPORT_UNDERLYING_ACTION, strats[i], amountToReport), @@ -137,6 +201,8 @@ contract XChainHub is LayerZeroApp { } } + /// @dev this looks like it completes the exit process but it's not + /// clear how that is useful in the context of rest of the contract function finalizeWithdrawFromVault(IVault vault) external onlyOwner { uint256 round = vault.batchBurnRound(); IERC20 underlying = vault.underlying(); @@ -148,6 +214,18 @@ contract XChainHub is LayerZeroApp { withdrawnPerRound[address(vault)][round] = withdrawn; } + /// @notice makes a deposit of the underyling token into the vault on a given chain + /// @dev this function handles the swap using anyswap, then a notification message sent using LayerZero + /// @dev currently calls the anyswap router, needs refactoring to stargate + /// @param dstChainId the layerZero chain id PROBABLY BROKEN + /// @dev bug? the dstChainId for layerZero and the anyswapRouter are likely not the same value + /// specifically, layerzero uses a uint16 and a custom set of chainIds, while + /// anyswap uses a uint256 which I am guessing allows us to use the standard chain ids + /// @param dstVault is the address of the vault on the destinatin chain + /// @param amount is the amount to deposit in underlying tokens + /// @param minOut how not to get rekt by miners + /// @param adapterParams additional LayerZero data + /// @param refundAddress if LayerZero has surplus function depositToChain( uint16 dstChainId, address dstVault, @@ -183,6 +261,9 @@ contract XChainHub is LayerZeroApp { ); } + /// @notice make a request to withdraw tokens from a vault on a specified chain + /// the actual withdrawal takes place once the batch burn process is completed + /// @dev see the _requestWithdrawAction for detail function withdrawFromChain( uint16 dstChainId, address dstVault, @@ -197,13 +278,15 @@ contract XChainHub is LayerZeroApp { _lzSend( dstChainId, - abi.encode(1, dstVault, msg.sender, amount), + abi.encode(REQUEST_WITHDRAW_ACTION, dstVault, msg.sender, amount), refundAddress, address(0), adapterParams ); } + /// @notice provided a successful batch burn has been executed, sends a message to + /// a vault to release the underlying tokens to the user, on a given chain. function finalizeWithdrawFromChain( uint16 dstChainId, address dstVault, @@ -218,13 +301,24 @@ contract XChainHub is LayerZeroApp { _lzSend( dstChainId, - abi.encode(2, dstVault, msg.sender), + abi.encode(FINALIZE_WITHDRAW_ACTION, dstVault, msg.sender), refundAddress, address(0), adapterParams ); } + /*/////////////////////////////////////////////////////////////// + Reducer + //////////////////////////////////////////////////////////////*/ + + /// @notice called by the Lz application on the dstChain, + /// then executes the corresponding action. + /// @param _srcChainId the layerZero chain id + /// @param _srcAddress UNUSED PARAM + /// @param _nonce UNUSED PARAM + /// @param _payload bytes encoded Message to be passed to the action + /// @dev do not confuse _payload with Message.payload, these are encoded separately function _nonblockingLzReceive( uint16 _srcChainId, bytes memory _srcAddress, @@ -251,10 +345,21 @@ contract XChainHub is LayerZeroApp { // receive report _reportUnderlyingAction(message.payload); } else { - revert(); + revert("XChainHub: Unrecognised Action on lzRecieve"); } } + /*/////////////////////////////////////////////////////////////// + Action Functions + //////////////////////////////////////////////////////////////*/ + + + /// @param _srcChainId what layerZero chainId was the request initiated from + /// @param _payload abi encoded as follows: + /// IVault + /// address (of strategy) + /// uint256 (amount to deposit) + /// uint256 (min amount of shares expected to be minted) function _depositAction(uint16 _srcChainId, bytes memory _payload) internal { @@ -283,6 +388,12 @@ contract XChainHub is LayerZeroApp { sharesPerStrategy[_srcChainId][strategy] += mintedShares; } + /// @notice enter the batch burn for a vault on the current chain + /// @param _srcChainId layerZero chain id where the request originated + /// @param _payload encoded in the format: + /// IVault + /// address (of strategy) + /// uint256 (amount of auxo tokens to burn) function _requestWithdrawAction(uint16 _srcChainId, bytes memory _payload) internal { @@ -309,6 +420,7 @@ contract XChainHub is LayerZeroApp { "XChainHub: strategy is already exiting from a previous round" ); + // update the state before executing the burn currentRoundPerStrategy[_srcChainId][strategy] = round; sharesPerStrategy[_srcChainId][strategy] -= amount; exitingSharesPerStrategy[_srcChainId][strategy] += amount; @@ -316,6 +428,13 @@ contract XChainHub is LayerZeroApp { vault.enterBatchBurn(amount); } + /// @notice executes a withdrawal of underlying tokens from a vault + /// @dev probably bugged unless anyswap and Lz use the same chainIds + /// @dev calls the anyswap router so will need to be replaced by Stargate Router + /// @param _srcChainId what layerZero chainId was the request initiated from + /// @param _payload abi encoded as follows: + /// IVault + /// address (of strategy) function _finalizeWithdrawAction(uint16 _srcChainId, bytes memory _payload) internal { @@ -339,16 +458,23 @@ contract XChainHub is LayerZeroApp { require(currentRound > 0, "XChainHub: no withdraws for strategy"); + // why are we resetting the current round? currentRoundPerStrategy[_srcChainId][strategy] = 0; exitingSharesPerStrategy[_srcChainId][strategy] = 0; IERC20 underlying = vault.underlying(); + // calculate the amount based on existing shares and current batch burn round IVault.BatchBurn memory batchBurn = vault.batchBurns(currentRound); uint256 amountPerShare = batchBurn.amountPerShare; uint256 strategyAmount = (amountPerShare * exitingShares) / (10**vault.decimals()); + + // approve and swap the tokens + // This might not work as I belive the anyswap router is expecting the address of an anyToken + // which needs an `.underlying()` method to be defined + // see https://github.com/anyswap/anyswap-v1-core/blob/d5f40f9a29212f597149f3cee9f8d9df1b108a22/contracts/AnyswapV6Router.sol#L310 underlying.safeApprove(address(anyswapRouter), strategyAmount); anyswapRouter.anySwapOutUnderlying( address(underlying), @@ -358,6 +484,8 @@ contract XChainHub is LayerZeroApp { ); } + /// @notice underlying holdings are updated on another chain and this function is broadcast + /// to all other chains for the strategy. function _reportUnderlyingAction(bytes memory payload) internal { (IStrategy strategy, uint256 amountToReport) = abi.decode( payload, diff --git a/src/strategy/BaseStrategy.sol b/src/strategy/BaseStrategy.sol index 6994e5b..7961a12 100644 --- a/src/strategy/BaseStrategy.sol +++ b/src/strategy/BaseStrategy.sol @@ -76,7 +76,7 @@ abstract contract BaseStrategy is ReentrancyGuard { ); /// @notice Event emitted when underlying is deposited in this strategy. - event Deposit(address indexed vault, uint256 amount); + event Deposited(address indexed vault, uint256 amount); /// @notice Event emitted when underlying is withdrawn from this strategy. event Withdraw(address indexed vault, uint256 amount); @@ -142,7 +142,7 @@ abstract contract BaseStrategy is ReentrancyGuard { depositedUnderlying += amount; underlying.safeTransferFrom(msg.sender, address(this), amount); - emit Deposit(msg.sender, amount); + emit Deposited(msg.sender, amount); success = SUCCESS; }