diff --git a/contracts/InstadappAdapter.sol b/contracts/InstadappAdapter.sol new file mode 100644 index 0000000..30ff476 --- /dev/null +++ b/contracts/InstadappAdapter.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import {IDSA} from "./interfaces/IDSA.sol"; + +/// @title InstadappAdapter +/// @author Connext +/// @notice This contract is inherited by InstadappTarget, it includes the logic to verify signatures +/// and execute the calls. +/// @dev This contract is not meant to be used directly, it is meant to be inherited by other contracts. +/// @custom:experimental This is an experimental contract. +contract InstadappAdapter is EIP712 { + /// Structs + /// @dev This struct is used to encode the data for InstadappTarget.cast function. + /// @param _targetNames The names of the targets that will be called. + /// @param _datas The data that will be sent to the targets. + /// @param _origin The address that will be used as the origin of the call. + struct CastData { + string[] _targetNames; + bytes[] _datas; + address _origin; + } + + /// @dev This struct is used to encode the data that is signed by the auth address. + /// The signature is then verified by the verify function. + struct Sig { + CastData castData; + bytes32 salt; + uint256 deadline; + } + + /// Storage + /// @dev This mapping is used to prevent replay attacks. + mapping(bytes32 => bool) private sigReplayProtection; + + /// Constants + /// @dev This is the typehash for the CastData struct. + bytes32 public constant CASTDATA_TYPEHASH = + keccak256("CastData(string[] _targetNames,bytes[] _datas,address _origin)"); + + /// @dev This is the typehash for the Sig struct. + bytes32 public constant SIG_TYPEHASH = + keccak256( + "Sig(CastData cast,bytes32 salt, uint256 deadline)CastData(string[] _targetNames,bytes[] _datas,address _origin)" + ); + + /// Constructor + constructor() EIP712("InstaTargetAuth", "1") {} + + /// Public functions + /// @dev This function is used to verify the signature. + /// @param auth The address of the auth. + /// @param signature The signature of the auth. + /// @param castData The data that will be sent to the targets. + /// @param salt The salt that will be used to prevent replay attacks. + /// @param deadline The deadline that will be used to prevent replay attacks. + /// @return boolean that indicates if the signature is valid. + function verify( + address auth, + bytes memory signature, + CastData memory castData, + bytes32 salt, + uint256 deadline + ) public view returns (bool) { + bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(SIG_TYPEHASH, hash(castData), salt, deadline))); + address signer = ECDSA.recover(digest, signature); + return signer == auth; + } + + /// @dev This function is used to hash the CastData struct. + /// @param castData The data that will be sent to the targets. + /// @return bytes32 that is the hash of the CastData struct. + function hash(CastData memory castData) public pure returns (bytes32) { + return keccak256(abi.encode(CASTDATA_TYPEHASH, castData._targetNames, castData._datas, castData._origin)); + } + + /// Internal functions + /// @dev This function is used to forward the call to dsa.cast function. + /// Cast the call is forwarded, the signature is verified and the salt is stored in the sigReplayProtection mapping. + /// @param dsaAddress The address of the DSA. + /// @param auth The address of the auth. + /// @param signature The signature by the auth. This signature is used to verify the SIG data. + /// @param castData The data that will be sent to the targets. + /// @param salt The salt that will be used to prevent replay attacks. + /// @param deadline The deadline that will be used to prevent replay attacks. + function authCast( + address dsaAddress, + address auth, + bytes memory signature, + CastData memory castData, + bytes32 salt, + uint256 deadline + ) internal { + IDSA dsa = IDSA(dsaAddress); + // check if Auth is valid, and included in the DSA + require(dsa.isAuth(auth), "Invalid Auth"); + + // check if signature is not replayed + require(!sigReplayProtection[salt], "Replay Attack"); + + // check if signature is not expired + require(block.timestamp <= deadline, "Signature Expired"); + + // check if signature is valid, and not replayed + require(verify(auth, signature, castData, salt, deadline), "Invalid signature"); + + // Signature Replay Protection + sigReplayProtection[salt] = true; + + // Cast the call + dsa.cast(castData._targetNames, castData._datas, castData._origin); + } +} diff --git a/contracts/InstadappTarget.sol b/contracts/InstadappTarget.sol new file mode 100644 index 0000000..d2396a6 --- /dev/null +++ b/contracts/InstadappTarget.sol @@ -0,0 +1,95 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IConnext} from "@connext/interfaces/core/IConnext.sol"; +import {IXReceiver} from "@connext/interfaces/core/IXReceiver.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {InstadappAdapter} from "./InstadappAdapter.sol"; + +/// @title InstadappTarget +/// @author Connext +/// @notice You can use this contract for cross-chain casting via dsa address +/// @dev This contract is used to receive funds from Connext +/// and forward them to Instadapp DSA via authCast function, In case of failure, +/// funds are forwarded to fallback address defined by the user under callData. +/// @custom:experimental This is an experimental contract. +contract InstadappTarget is IXReceiver, InstadappAdapter { + using SafeERC20 for IERC20; + /// Storage + /// @dev This is the address of the Connext contract. + IConnext public connext; + + /// Events + /// @dev This event is emitted when the authCast function is called. + event AuthCast(bytes32 transferId, address dsaAddress, address auth, bool success, bytes returnedData); + + /// Modifiers + /// @dev This modifier is used to ensure that only the Connext contract can call the function. + modifier onlyConnext() { + require(msg.sender == address(connext), "Caller must be Connext"); + _; + } + + /// Constructor + /// @param _connext The address of the Connext contract. + constructor(address _connext) { + connext = IConnext(_connext); + } + + /// Public functions + /// @dev This function is used to receive funds from Connext and forward them to DSA. + /// Then it forwards the call to authCast function. + /// @param _amount The amount of funds that will be received. + /// @param _asset The address of the asset that will be received. + /// @param _transferId The id of the transfer. + /// @param _callData The data that will be sent to the targets. + function xReceive( + bytes32 _transferId, + uint256 _amount, + address _asset, + address, + uint32, + bytes memory _callData + ) external onlyConnext returns (bytes memory) { + // Decode signed calldata + // dsaAddress: address of DSA contract + // auth: address of Authority, which whitelisted at dsaContract. + // signature: signature is signed by the auth includes the castData with salt. + // castData: CastData required for execution at destination + // salt: salt for Signature Replay Protection, which is unique to each signature signed by auth. + // deadline: deadline for the cast to be valid + ( + address dsaAddress, + address auth, + bytes memory signature, + CastData memory _castData, + bytes32 _salt, + uint256 deadline + ) = abi.decode(_callData, (address, address, bytes, CastData, bytes32, uint256)); + + // verify the dsaAddress + require(dsaAddress != address(0), "!invalidFallback"); + + // transfer funds to this dsaAddress + SafeERC20.safeTransfer(IERC20(_asset), dsaAddress, _amount); + + // forward call to AuthCast + // calling via encodeWithSignature as alternative to try/catch + (bool success, bytes memory returnedData) = address(this).call( + abi.encodeWithSignature( + "authCast(address,address,bytes,CastData,bytes32, uint256)", + dsaAddress, + auth, + signature, + _castData, + _salt, + deadline + ) + ); + + emit AuthCast(_transferId, dsaAddress, auth, success, returnedData); + + return returnedData; + } +} diff --git a/contracts/interfaces/IDSA.sol b/contracts/interfaces/IDSA.sol new file mode 100644 index 0000000..bc993e3 --- /dev/null +++ b/contracts/interfaces/IDSA.sol @@ -0,0 +1,12 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IDSA { + function cast( + string[] calldata _targetNames, + bytes[] calldata _datas, + address _origin + ) external payable returns (bytes32); + + function isAuth(address user) external view returns (bool); +}