diff --git a/contracts/ServiceRegistry.sol b/contracts/ServiceRegistry.sol new file mode 100644 index 0000000..5468f93 --- /dev/null +++ b/contracts/ServiceRegistry.sol @@ -0,0 +1,179 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity >=0.7.6; +import "hardhat/console.sol"; + +contract ServiceRegistry { + mapping(bytes32 => uint256) public lastExecuted; + mapping(address => bool) private trustedAddresses; + mapping(bytes32 => address) private namedService; + address public owner; + + uint256 public requiredDelay = 0; //big enaugh that any power of miner over timestamp does not matter + + modifier validateInput(uint256 len) { + require(msg.data.length == len, "illegal-padding"); + _; + } + + modifier delayedExecution() { + bytes32 operationHash = keccak256(msg.data); + uint256 reqDelay = requiredDelay; + + if (lastExecuted[operationHash] == 0 && reqDelay > 0) { + //not called before, scheduled for execution + // solhint-disable-next-line not-rely-on-time + lastExecuted[operationHash] = block.timestamp; + emit ChangeScheduled( + msg.data, + operationHash, + // solhint-disable-next-line not-rely-on-time + block.timestamp + reqDelay + ); + } else { + require( + // solhint-disable-next-line not-rely-on-time + block.timestamp - reqDelay > lastExecuted[operationHash], + "delay-to-small" + ); + // solhint-disable-next-line not-rely-on-time + emit ChangeApplied(msg.data, block.timestamp); + _; + lastExecuted[operationHash] = 0; + } + } + + modifier onlyOwner() { + require(msg.sender == owner, "only-owner"); + _; + } + + constructor(uint256 initialDelay) { + require(initialDelay < type(uint256).max,"risk-of-overflow"); + requiredDelay = initialDelay; + owner = msg.sender; + } + + function transferOwnership(address newOwner) + public + onlyOwner + validateInput(36) + delayedExecution + { + owner = newOwner; + } + + function changeRequiredDelay(uint256 newDelay) + public + onlyOwner + validateInput(36) + delayedExecution + { + requiredDelay = newDelay; + } + + function addTrustedAddress(address trustedAddress) + public + onlyOwner + validateInput(36) + delayedExecution + { + trustedAddresses[trustedAddress] = true; + } + + function removeTrustedAddress(address trustedAddress) + public + onlyOwner + validateInput(36) + { + trustedAddresses[trustedAddress] = false; + } + + function isTrusted(address testedAddress) public view returns (bool) { + return trustedAddresses[testedAddress]; + } + + function getServiceNameHash(string memory name) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(name)); + } + + function addNamedService(bytes32 serviceNameHash, address serviceAddress) + public + onlyOwner + validateInput(68) + delayedExecution + { + require( + namedService[serviceNameHash] == address(0), + "service-override" + ); + namedService[serviceNameHash] = serviceAddress; + } + + function updateNamedService(bytes32 serviceNameHash, address serviceAddress) + public + onlyOwner + validateInput(68) + delayedExecution + { + require( + namedService[serviceNameHash] != address(0), + "service-does-not-exist" + ); + namedService[serviceNameHash] = serviceAddress; + } + + function removeNamedService(bytes32 serviceNameHash) + public + onlyOwner + validateInput(36) + { + require( + namedService[serviceNameHash] != address(0), + "service-does-not-exist" + ); + namedService[serviceNameHash] = address(0); + emit RemoveApplied(serviceNameHash); + } + + function getRegisteredService(string memory serviceName) + public + view + returns (address) + { + address retVal = getServiceAddress( + keccak256(abi.encodePacked(serviceName)) + ); + return retVal; + } + + function getServiceAddress(bytes32 serviceNameHash) + public + view + returns (address) + { + return namedService[serviceNameHash]; + } + + function clearScheduledExecution(bytes32 scheduledExecution) + public + onlyOwner + validateInput(36) + { + require(lastExecuted[scheduledExecution] > 0, "execution-not-sheduled"); + lastExecuted[scheduledExecution] = 0; + emit ChangeCancelled(scheduledExecution); + } + + event ChangeScheduled( + bytes data, + bytes32 dataHash, + uint256 firstPossibleExecutionTime + ); + event ChangeCancelled(bytes32 data); + event ChangeApplied(bytes data, uint256 firstPossibleExecutionTime); + event RemoveApplied(bytes32 nameHash); +} diff --git a/contracts/actions/ActionBase.sol b/contracts/actions/ActionBase.sol new file mode 100644 index 0000000..87a950a --- /dev/null +++ b/contracts/actions/ActionBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.7.6; +pragma abicoder v2; + +abstract contract ActionBase { + enum ActionType { + FLASHLOAN, + DEFAULT + } + + function executeAction(bytes[] memory _callData) public payable virtual returns (bytes32); + + function actionType() public pure virtual returns (uint8); +} diff --git a/contracts/actions/OperationRunner.sol b/contracts/actions/OperationRunner.sol new file mode 100644 index 0000000..c0cfdc3 --- /dev/null +++ b/contracts/actions/OperationRunner.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "../interfaces/mcd/IJoin.sol"; +import "../interfaces/mcd/IManager.sol"; +import "./ActionBase.sol"; +import "hardhat/console.sol"; +import "../ServiceRegistry.sol"; + +struct Operation { + string name; + bytes[][] callData; + bytes32[] actionIds; + address serviceRegistryAddr; +} + +contract OperationRunner { + function executeOperation(Operation memory operation) public payable { + _executeActions(operation); + } + + function _executeActions(Operation memory operation) internal { + // address firstActionAddr = ServiceRegistry(operation.serviceRegistryAddr) + // .getServiceAddress(operation.actionIds[0]); + + bytes32[] memory returnValues = new bytes32[](operation.actionIds.length); + + // if (isFlashLoanAction(firstActionAddr)) { + // executeFlashloan(operation, firstActionAddr, returnValues); + // } else { + for (uint256 i = 0; i < operation.actionIds.length; ++i) { + returnValues[i] = _executeAction(operation, i, returnValues); + // _executeAction(operation, i); + } + // } + } + + function _executeAction( + Operation memory operation, + uint256 _index, + bytes32[] memory returnValues + ) internal returns (bytes32 response) { + address actionAddress = ServiceRegistry(operation.serviceRegistryAddr).getServiceAddress( + operation.actionIds[_index] + ); + + actionAddress.delegatecall( + abi.encodeWithSignature("executeAction(bytes[])", operation.callData[_index]) + ); + + return ""; + } + + function isFlashLoanAction(address actionAddr) internal pure returns (bool) { + return ActionBase(actionAddr).actionType() == uint8(ActionBase.ActionType.FLASHLOAN); + } +} diff --git a/contracts/actions/deposit.sol b/contracts/actions/deposit.sol new file mode 100644 index 0000000..8bde72b --- /dev/null +++ b/contracts/actions/deposit.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "../interfaces/mcd/IJoin.sol"; +import "../interfaces/mcd/IManager.sol"; +import "./ActionBase.sol"; +import "hardhat/console.sol"; + +contract Deposit is ActionBase { + function executeAction(bytes[] memory _callData) + public + payable + override + returns ( + // ) public payable virtual override returns (bytes32) { + bytes32 + ) + { + (address joinAddr, address mcdManager) = parseInputs(_callData); + // address joinAddr = 0x2F0b23f53734252Bda2277357e97e1517d6B042A; + // address mcdManager = 0x5ef30b9986345249bc32d8928B7ee64DE9435E39; + + // // joinAddr = _parseParamAddr(joinAddr, _paramMapping[0], _subData, _returnValues); + + console.log("address this", address(this)); + + uint256 newVaultId = _mcdOpen(joinAddr, mcdManager); + + return bytes32(newVaultId); + } + + function actionType() public pure override returns (uint8) { + return uint8(ActionType.DEFAULT); + } + + function _mcdOpen(address _joinAddr, address _mcdManager) internal returns (uint256 vaultId) { + bytes32 ilk = IJoin(_joinAddr).ilk(); + vaultId = IManager(_mcdManager).open(ilk, address(this)); + } + + function parseInputs(bytes[] memory _callData) + internal + pure + returns (address joinAddr, address mcdManager) + { + joinAddr = abi.decode(_callData[0], (address)); + mcdManager = abi.decode(_callData[1], (address)); + } +} diff --git a/contracts/actions/flashloan.sol b/contracts/actions/flashloan.sol new file mode 100644 index 0000000..f6c2db6 --- /dev/null +++ b/contracts/actions/flashloan.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "../interfaces/mcd/IJoin.sol"; +import "../interfaces/mcd/IManager.sol"; +import "./ActionBase.sol"; +import "hardhat/console.sol"; + +contract Flashloan is ActionBase { + function actionType() public pure override returns (uint8) { + return uint8(ActionType.FLASHLOAN); + } + + function executeAction(bytes[] memory _callData) + public + payable + override + returns ( + // ) public payable virtual override returns (bytes32) { + bytes32 + ) + { + return ""; + } +} diff --git a/contracts/actions/openVault.sol b/contracts/actions/openVault.sol new file mode 100644 index 0000000..65a14bc --- /dev/null +++ b/contracts/actions/openVault.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "../interfaces/mcd/IJoin.sol"; +import "../interfaces/mcd/IManager.sol"; +import "./ActionBase.sol"; +import "hardhat/console.sol"; + +contract OpenVault is ActionBase { + function executeAction(bytes[] memory _callData) public payable override returns (bytes32) { + // (address joinAddr, address mcdManager) = parseInputs(_callData); + + address joinAddr = 0x2F0b23f53734252Bda2277357e97e1517d6B042A; + address mcdManager = 0x5ef30b9986345249bc32d8928B7ee64DE9435E39; + + uint256 newVaultId = _mcdOpen(joinAddr, mcdManager); + + return bytes32(newVaultId); + } + + function actionType() public pure override returns (uint8) { + return uint8(ActionType.DEFAULT); + } + + function _mcdOpen(address _joinAddr, address _mcdManager) internal returns (uint256 vaultId) { + bytes32 ilk = IJoin(_joinAddr).ilk(); + vaultId = IManager(_mcdManager).open(ilk, address(this)); + } + + function parseInputs(bytes[] memory _callData) + internal + pure + returns (address joinAddr, address mcdManager) + { + joinAddr = abi.decode(_callData[0], (address)); + mcdManager = abi.decode(_callData[1], (address)); + } +} diff --git a/test/actions-poc-test.ts b/test/actions-poc-test.ts new file mode 100644 index 0000000..29bb1cc --- /dev/null +++ b/test/actions-poc-test.ts @@ -0,0 +1,129 @@ +import { expect } from 'chai' +import BigNumber from 'bignumber.js' +import { ethers } from 'hardhat' +import { JsonRpcProvider } from '@ethersproject/providers' +import { Contract, Signer } from 'ethers' +import MAINNET_ADDRESSES from '../addresses/mainnet.json' +import { + deploySystem, + getOraclePrice, + dsproxyExecuteAction, + getLastCDP, + findMPAEvent, + DeployedSystemInfo, +} from './common/utils/mcd-deployment.utils' +import { + amountToWei, + calculateParamsIncreaseMP, + calculateParamsDecreaseMP, + prepareMultiplyParameters, + ensureWeiFormat, +} from './common/utils/params-calculation.utils' +import { balanceOf } from './utils' + +import ERC20ABI from '../abi/IERC20.json' +import { getVaultInfo } from './common/utils/mcd.utils' +import { expectToBe, expectToBeEqual } from './common/utils/test.utils' +import { one } from './common/cosntants' + +const LENDER_FEE = new BigNumber(0) + +async function checkMPAPostState(tokenAddress: string, mpaAddress: string) { + return { + daiBalance: await balanceOf(MAINNET_ADDRESSES.MCD_DAI, mpaAddress), + collateralBalance: await balanceOf(tokenAddress, mpaAddress), + } +} + +describe('Multiply Proxy Action with Mocked Exchange', async () => { + const oazoFee = 2 // divided by base (10000), 1 = 0.01%; + const oazoFeePct = new BigNumber(oazoFee).div(10000) + const flashLoanFee = LENDER_FEE + const slippage = new BigNumber(0.0001) // percentage + + let provider: JsonRpcProvider + let signer: Signer + let address: string + let system: DeployedSystemInfo + let exchangeDataMock: { to: string; data: number } + let DAI: Contract + + let CDP_ID: number // this test suite operates on one Vault that is created in first test case (opening Multiply Vault) + let CDP_ILK: string + + before(async () => { + provider = new ethers.providers.JsonRpcProvider() + signer = provider.getSigner(0) + DAI = new ethers.Contract(MAINNET_ADDRESSES.MCD_DAI, ERC20ABI, provider).connect(signer) + address = await signer.getAddress() + + provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl: process.env.ALCHEMY_NODE, + blockNumber: 13274574, + }, + }, + ]) + + system = await deploySystem(provider, signer, true) + + + // exchangeDataMock = { + // to: system.exchangeInstance.address, + // data: 0, + // } + // // await system.exchangeInstance.setSlippage(0); + // // await system.exchangeInstance.setMode(0); + + // await system.exchangeInstance.setFee(oazoFee) + }) + + describe(`opening Multiply Vault`, async () => { + const marketPrice = new BigNumber(2380) + const currentColl = new BigNumber(100) // STARTING COLLATERAL AMOUNT + const currentDebt = new BigNumber(0) // STARTING VAULT DEBT + let oraclePrice: BigNumber + + before(async () => { + oraclePrice = await getOraclePrice(provider) + + await system.exchangeInstance.setPrice( + MAINNET_ADDRESSES.ETH, + amountToWei(marketPrice).toFixed(0), + ) + }) + + it(`should open vault with required collateralisation ratio`, async () => { + const callData1 = ethers.utils.defaultAbiCoder.encode(["address"], [MAINNET_ADDRESSES.MCD_JOIN_ETH_A]) + const callData2 = ethers.utils.defaultAbiCoder.encode(["address"], [MAINNET_ADDRESSES.CDP_MANAGER]) + const openVaultCalldata = system.actionOpenVault.interface.encodeFunctionData("executeAction", [[callData1, callData2]]); + + const openVaultHashName = await system.serviceRegistry.getServiceNameHash('OPEN_VAULT'); + await system.serviceRegistry.addNamedService( + openVaultHashName, + system.actionOpenVault.address, + ) + + const dsproxy_calldata = system.operationRunner.interface.encodeFunctionData("executeOperation", + [{ + name: 'openVaultOperation', + callData: [[openVaultCalldata],[openVaultCalldata]], + actionIds: [openVaultHashName, openVaultHashName], + serviceRegistryAddr: system.serviceRegistry.address, + }] + ) + + const tx = await system.dsProxyInstance['execute(address,bytes)'](system.operationRunner.address, dsproxy_calldata, { + from: address, + value: ensureWeiFormat(0), + gasLimit: 8500000, + gasPrice: 1000000000, + }) + + const lastCDP = await getLastCDP(provider, signer, system.userProxyAddress) + + console.log('LAST CDP', lastCDP ); + }) + }) +}) diff --git a/test/common/utils/mcd-deployment.utils.ts b/test/common/utils/mcd-deployment.utils.ts index e38239b..d7da3d1 100644 --- a/test/common/utils/mcd-deployment.utils.ts +++ b/test/common/utils/mcd-deployment.utils.ts @@ -353,6 +353,9 @@ export interface DeployedSystemInfo { wethTokenInstance: Contract } guni: Contract + actionOpenVault: Contract + operationRunner: Contract + serviceRegistry: Contract } export async function deploySystem( @@ -417,6 +420,23 @@ export async function deploySystem( ]) } + + // ACTIONS POC deployed contracts + + const ActionOpenVault = await ethers.getContractFactory('OpenVault', signer) + const actionOpenVault = await ActionOpenVault.deploy() + deployedContracts.actionOpenVault = await actionOpenVault.deployed() + + + const OperationRunner = await ethers.getContractFactory('OperationRunner', signer) + const operationRunner = await OperationRunner.deploy() + deployedContracts.operationRunner = await operationRunner.deployed() + + const ServiceRegistry = await ethers.getContractFactory('ServiceRegistry', signer) + const serviceRegistry = await ServiceRegistry.deploy([0]) + deployedContracts.serviceRegistry = await serviceRegistry.deployed() + + return deployedContracts as DeployedSystemInfo }