**USDS upgrade for xDAI Bridge- contract implementation: switching from DAI to USDS & New Bridge Router** >> Warning: This version is deprecated! Please check [here](https://github.com/gnosischain/tokenbridge-contracts/blob/xdaibridge/USDSMigration.md) for up to date version. # Overview 1. New Router contract(BridgeRouter.sol) to route `relayTokens` function and claim token `executeSignatuers` to xDAI bridge or Omnibridge, or others in future upgrade 1. Router contract is controlled by bridge governors and upgradeable 2. Routes: 1. DAI → XDaiBridgePeripheral 2. USDS → XDaiBridge 3. ETH →WETHOmnibridgeRouter 4. Other tokens → Omnibridge 2. xDAI bridge only accepts deposit from USDS token. 1. Function interfaces are not changed, but only changing the address of `daiToken()` to USDS and `sDaiToken()` to sUSDS. ## Deposit Flow (Relay Token) 1. User approve router contract `token.approve(router, amount)` 2. User interact with Router contract and specify the token address: `relayTokens(token, receiver, amount` 3. Routes: 1. token = DAI, router contract calls Peripheral contract to swap Dai To USDS and call xDAI bridge relayToken. 2. token = USDS, router contract calls XDAI bridge directly. 3. token = ETH, router contract calls WETHOmnibridgeRouter. 4. token = others, router contract calls Omnibridge. ![USDSupgrade-Router-deposit](https://hackmd.io/_uploads/SkcsbFe9yl.png) ## Invest Flow 1. Bridge Upgrade: During the bridge upgrade, all the current sDAI hold by the bridge will be converted to sUSDS. This involves 3 external function calls by bridge governor: `swapSDAIToUSDS()` + `initializeInterest(USDS)`+ `invest(USDS)` . 2. Invest USDS: By calling `investDai()` in xDAI bridge contract, USDS from the bridge will be deposited into sUSDS contract and hold sUSDS in return. 3. Refill Bridge: When the amount of USDS is below minCashThreshold, anyone can refill the bridge, and sUSDS will be withdrawn from sUSDS contract to USDS. 4. Pay Interest: Checks the USDS interest amount that can be relayed to Gnosis Chain and relays to interest receiver(controlled by Karpatkey) on Gnosis Chain. ![USDSupgrade-InvestUSDS-invest](https://hackmd.io/_uploads/ryunWtl9kx.png) ## Withdraw Flow (Claim tokens) 1. User calls Router contract to claim their token back: 1. When user calls `executeSignatures` , either: 1. USDS is transferred to receiver in xDAI bridge case 2. relevant token is transferred to receiver in Omnibridge case > The parameter for `executeSignatures(bytes message, bytes signatures)` doesn’t change, as the Message struct and signature verification logic remains the same. > 2. If user wants to receive DAI instead, calls `executeSignaturesAndSwapToDai(bytes message, bytes signatures, bytes permiSignatures)`: 1. Router calls Peripheral to swap USDS to DAI and transfers back to receiver. 2. receiver of the token need to sign permit message to allow xDAI bridge peripheral contract to swap Usds to Dai on behalf of owner. > `permitSignatures` is ECDSA signature if receiver is an EOA, and verified by ecrecover. While and for smart contract the signature is [smart contract signature](https://docs.safe.global/sdk/protocol-kit/guides/signatures/messages#on-chain-messages) and verified by ERC1271 `isValidSignature`. > ![USDSupgrade-Router-ExecuteSignatures.withdraw](https://hackmd.io/_uploads/ry-abKx91e.png) ## Impact 1. Contract level on Ethereum: 1. Function interface: `relayTokens` , `executeSignatures` on XDaiForeignBridge.sol remains the same. 1. Contract value: 1. erc20Token(), daiToken() now returns USDS instead of DAI 🔴 2. sDaiToken() now returns sUSDS. 3. the bridge parameters indicate the value for USDS i.e. dailyLimit, executionDailyLimit, maxPerTx, etc. 2. New `BridgeRouter` and `XdaiBridgePeripheral` contracts 2. Contract level on Gnosis Chain: No impact 3. Validator level: No impact 4. Application level on Ethereum: 1. `BridgeRouter` contract can be used as an entry point to reduce complexity of interacting with two different bridges (XDai bridge & Omnibridge) 2. Increase of gas compares to interacting with the bridge directly. 3. To adapt to upgrade, application can either: 1. Change the entry point to BridgeRouter and call `BridgeRouter.relayTokens(token, recipient, amount)` instead of `xDAIForeignBridge.relayTokens(recipient, amount)` 2. Receive USDS by default by calling `xDAIForeignBridge.relayTokens(recipient, amount)` as usual, and swap through `DaiUsds.UsdsToDai()` contract themselves. ## Risk & Dependency 1. DaiUsds controlled by Sky is unable to swap Dai ↔ Usds 1. Unlikely to happen 2. If new DaiUsds contract is deployed, XDaiBridgePeripheral contract need to be redeployed and update route in `BridgeRouter.setRoute()` 2. `BridgeRouter` and `XdaiBridgePeripheral` should be managed by Gnosis team. # Contract implementation ## > Disclaimer: Please refer to the tokenbridge-contracts repo for the latest commits ## > https://github.com/gnosischain/tokenbridge-contracts/tree/feat/xdai-usds-migration ### ForeignXDAIBridge ```jsx function swapSDAIToUSDS() public { bytes32 isUSDSBridgeUpgrade = keccak256("upgrade_DAI_to_USDS"); require(!boolStorage[isUSDSBridgeUpgrade], "USDS bridge ugprade completed"); address sDAI = 0x83F20F44975D03b1b09e64809B757c47f942BEeA; address DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address DaiUsds = 0x3225737a9Bbb6473CB4a45b7244ACa2BeFdB276A; // withdraw all sDAI into DAI uint256 maxWithdrawable = ISavingsDai(sDAI).maxWithdraw(address(this)); ISavingsDai(sDAI).withdraw(maxWithdrawable, address(this), address(this)); // disableInterest for DAI _setInvestedAmount(DAI, 0); _setInterestEnabled(DAI, false); // swap DAI -> USDS uint256 remainDAI = ERC20(DAI).balanceOf(address(this)); ERC20(DAI).approve(DaiUsds, remainDAI); IDaiUsds(DaiUsds).daiToUsds(address(this), remainDAI); boolStorage[isUSDSBridgeUpgrade] = true; } ``` ### BridgeRouter contract The BridgeRouter contract is upgradeable and use TransparentUpgradeableProxy from openzeppelin. ```jsx pragma solidity ^0.8.0; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { IForeignBridge } from "../../interfaces/IForeignBridge.sol"; import { IXDaiBridgePeripheral } from "../../interfaces/IXDaiBridgePeripheral.sol"; import { IERC20 } from "../../interfaces/IERC20.sol"; /// @title BridgeRouter /// @author Gnosis Chain Bridge team /// @notice A router contract that facilitates the correct routing for specific token that bridges to Gnosis Chain /// @dev this intended to be an upgradeable contract contract BridgeRouter is OwnableUpgradeable { address public immutable FOREIGN_OMNIBRIDGE = 0x88ad09518695c6c3712AC10a214bE5109a655671; address public immutable FOREIGN_AMB = 0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e; address public immutable FOREIGN_XDAIBRIDGE = 0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016; address public immutable DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address public immutable USDS = 0xdC035D45d973E3EC169d2276DDab16f1e407384F; uint256[49] __gap; mapping(address => address) public tokenRoutes; function initialize() public initializer { __Ownable_init(msg.sender); } /// @notice An entry point contract for user to bridge any token from source chain /// @dev directs route to relevant contract to perform token relaying /// @param _token token to bridge /// @param _receiver receiver of token on Gnosis Chain /// @param _amount amount to receive on Gnosis Chain function relayTokens(address _token, address _receiver, uint256 _amount) external { address route = tokenRoutes[_token]; if (_token == DAI) { IERC20(_token).transferFrom(msg.sender, route, _amount); IXDaiBridgePeripheral(route).relayTokens(_receiver, _amount); } else if (_token == USDS) { // token need to be transferred to router contract first, because the bridge will call transferFrom(msg.sender, bridge, amount); IERC20(_token).transferFrom(msg.sender, address(this), _amount); IERC20(_token).approve(route, _amount); IXDaiBridgePeripheral(route).relayTokens(_receiver, _amount); } else { IERC20(_token).transferFrom(msg.sender, address(this), _amount); IERC20(_token).approve(FOREIGN_OMNIBRIDGE, _amount); IForeignBridge(FOREIGN_OMNIBRIDGE).relayTokens(_token, _receiver, _amount); } } /// @notice set route for specific token /// @param _token token address /// @param _route router contract address function setRoute(address _token, address _route) public onlyOwner { require(_route != address(0) && _route != address(this), "invalid route address"); uint256 size; assembly { size := extcodesize(_route) } require(size > 0, "route should be a contract"); tokenRoutes[_token] = _route; } /// @notice claim function /// @dev this function check if the data belongs of xDAI bridge or AMB/Omnibridge /// @param message for claiming tx /// @param signatures signatures from bridge validators function executeSignatures(bytes memory message, bytes memory signatures) external { if (message.length == 104) { // xdai bridge IForeignBridge(FOREIGN_XDAIBRIDGE).executeSignatures(message, signatures); } else { // amb & omnibridge IForeignBridge(FOREIGN_AMB).safeExecuteSignaturesWithAutoGasLimit(message, signatures); } } /// @notice claim function and receive DAI /// @dev receiver of the token should sign and submit signature to allow peripheral contract swapping Usds to Dai /// @param message data for claiming tx /// @param signatures signatures from bridge validators /// @param permitSignatures permit signature by token receiver function executeSignaturesAndSwapToDai(bytes memory message, bytes memory signatures, bytes memory permitSignatures) external { require(message.length == 104, "invalid message length"); address xdaiBridgePeripheral = tokenRoutes[DAI]; IXDaiBridgePeripheral(xdaiBridgePeripheral).executeSignaturesAndSwapToDai( message, signatures, permitSignatures ); } /// @notice Allows to transfer any locked token from this contract. /// @param token token to recover /// @param recipient recipient of token function recoverLockedFund(address token, address recipient) external onlyOwner { if(token == address(0)){ uint256 value = address(this).balance; require(value> 0, "zero balance"); require(payable(recipient).send(value), "unsuccesssful sent"); }else{ require(IERC20(token).balanceOf(address(this))> 0, "zero balance"); IERC20(token).transfer(recipient, IERC20(token).balanceOf(address(this))); } } } ``` ### Peripheral contract ```jsx pragma solidity ^0.8.0; import { IForeignBridge } from "../../interfaces/IForeignBridge.sol"; import { IERC20 } from "../../interfaces/IERC20.sol"; interface IDaiUsds { function daiToUsds(address usr, uint256 wad) external; function usdsToDai(address usr, uint256 wad) external; } /// @title XdaiBridgePeripheral /// @author Gnosis Chain's bridge team /// @notice A peripheral contract to allow user to deposit DAI, convert it into USDS and call XDai Bridge relayTokens /// @dev This contract is non upgradeable and only callable by BridgeRouter contract contract XDaiBridgePeripheral { address public router; address public DAIUSDS = 0x3225737a9Bbb6473CB4a45b7244ACa2BeFdB276A; address public FOREIGN_XDAIBRIDGE = 0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016; address public USDS = 0xdC035D45d973E3EC169d2276DDab16f1e407384F; address public DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; modifier onlyRouter() { require(msg.sender == router, "only Router"); _; } constructor(address _router) { router = _router; } /// @notice Convert Dai to Usds and relayTokens on behalf of user /// @dev only callable by BridgeRouter /// @param receiver receiver of the xDAI token on Gnosis Chain /// @param amount amount of xDAI token received function relayTokens(address receiver, uint256 amount) external onlyRouter { // swap Dai to Usds IERC20(DAI).approve(DAIUSDS, amount); IDaiUsds(DAIUSDS).daiToUsds(address(this), amount); // call XDaibridge relayTokens IERC20(USDS).approve(FOREIGN_XDAIBRIDGE, amount); IForeignBridge(FOREIGN_XDAIBRIDGE).relayTokens(receiver, amount); } /// @notice claim the USDS token and convert to Dai for user /// @dev receiver should permit this contract to convert Usds to Dai by submitting permitSignature /// @param message data about the claiming tx for `executeSignatures` /// @param signatures signatures from bridge valdiators /// @param permitSignatures signature signed by the receiver of the tx to permit this contract doing the swap action) function executeSignaturesAndSwapToDai(bytes memory message, bytes memory signatures, bytes memory permitSignatures) external onlyRouter { require(message.length == 104, "Invalid data length"); IForeignBridge(FOREIGN_XDAIBRIDGE).executeSignatures(message, signatures); address recipient; uint256 amount; bytes32 nonce; address contractAddress; assembly { recipient := mload(add(message, 20)) amount := mload(add(message, 52)) nonce := mload(add(message, 84)) contractAddress := mload(add(message, 104)) } IERC20(USDS).permit(recipient, address(this), amount, block.timestamp + 1 days, permitSignatures); IERC20(USDS).transferFrom(recipient, address(this), amount); IERC20(USDS).approve(DAIUSDS, amount); IDaiUsds(DAIUSDS).usdsToDai(recipient, amount); } } ``` **Repository UR**L https://github.com/gnosischain/tokenbridge-contracts/tree/feat/xdai-usds-migration PR: https://github.com/gnosischain/tokenbridge-contracts/pull/7 **New contracts** 1. BridgeRouter: https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/contracts/upgradeable_contracts/erc20_to_native/BridgeRouter.sol , Upgradeable with TransparentUpgradeableProxy https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/contracts/upgradeability/TransparentUpgradeableProxy.sol 2. XDaiBridgePeripheral: https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/contracts/upgradeable_contracts/erc20_to_native/XDaiBridgePeripheral.sol **Modified contracts** 1. XDaiForeignBridge: https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/contracts/upgradeable_contracts/erc20_to_native/XDaiForeignBridge.sol 2. SavingsDaiConnector: https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/contracts/upgradeable_contracts/erc20_to_native/SavingsDaiConnector.sol **Fork Test** https://github.com/gnosischain/tokenbridge-contracts/blob/feat/xdai-usds-migration/test/foundry/forkTest.sh **Bridge validator test** (Check if bridge validator continues to work after bridge migration & xDAI can be still be minted on Gnosis Chain) https://github.com/gnosischain/xdai-bridge-test ## Guideline for bridge UI / 3rd party bridges ### Phase 1: use BridgeRouter contract as entry point, xDAIBridge still accept DAI by default 💡 **USDS is not yet supported in this phase** 1. Integrate Bridge Router `relayTokens(address token, address recipient, uint256 amount)` 2. For DAI deposit, still uses the current xDAI Foreign Bridge. 3. Period: 1 month ### Phase 2: upgrade xDAI bridge, USDS is the default token 💡**USDS is now the default token on xDAI bridge** 1. Don’t need to change function interface 2. Workflow: 1. Deposit: 1. IERC20(token).approve(router, amount); 2. router.relayTokens(address token, address recipient, uint256 amount) 2. Withdraw: 1. router.executeSignatures(message, signatures) 2. router.executeSignaturesAndSwapToDai(message, signatures, permitSignature) 1. permitSignature: signed by token recipient 1. if token recipient is EOA: ECDSA signature and verified by ecrecover 2. if token recipient is smart contract: smart contract signature and verified by EIP 1271.isValidSignature # Backward compatibility ## DAI compatiblility 1. If users need to relay DAI, they should use BridgeRouter.relayTokens(DAI, recipient, amount). 2. When user calls xDAIBridge.relayTokens() directly, but want to relay DAI token, 1. The transaction will revert if 1. user has no USDS.allowance for bridge 2. user has USDS.allowance for bridge bridge, but allowance < bridged amount 2. The transaction will NOT revert if 1. user has enough USDS allowance for bridge 3. In case 1b. , user’s USDS will be relayed by the bridge instead of DAI, to make sure that this doesn’t happen, user can either 1. Use Bridge Router contract and specify token as DAI in relayTokens() 2. If 3rd party application still uses xDAIBridge as entry point, the UI need to check if the USDS.allowance[user][bridge] ≥ amount, and notify user about the possibility of case 1b. (This shouldn’t happen on the UI level, because 3rd party application are expected to use BridgeRouter contract as entry point) # General rules for users/3rd party applications 1. **MUST** call `DAI.approve(bridgeRouter, amount)` + `BridgeRouter.relayTokens(DAI, recipient, amount)` , if wants to relay DAI. 2. **SHOULD** call **either** these two options to relay USDS: 1. `USDS.approve(bridgeRouter, amount)` + `BridgeRouter.relayTokens(USDS, recipient, amount)` 2. `USDS.approve(xDAIbridge, amount` + `xDAIBridge.relayTokens(recipient, amount)` 3. **SHOULD** call **either** these two options to relay ETH: 1. `BridgeRouter.relayTokens{value: amount}(address(0), recipient, amount)` 2. `WETHOmnibridgeRouter.wrapAndRelayTokens{value: amount}(recipient)` 4. **SHOULD** call **either** these two options to relay other tokens (GNO, USDC, USDT, etc): 1. `token.approve(bridgeRouter, amount)` + `BridgeRouter.relayTokens(token, recipient, amount)` 2. `token.approve(Omnibridge, amount` + `Omnibridge.relayTokens(token, recipient, amount)` 5. **MUST NOT** call `token.transfer` to Bridge Router, xDAI Bridge or Omnibridge contract, as it will not relay the token and cause the transferred token get locked in the contract. To claim back, it requires bridge governance procedure.