From cb5110a5ec38912264b02b622ba2ccee6196ff35 Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Thu, 12 Oct 2023 22:49:06 +0330 Subject: [PATCH 01/11] Adding multiple erc20 token option --- src/WorkingCapital.sol | 79 +++++++++++++++++++++++++++++-------- src/WorkingCapitalSetup.sol | 5 ++- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index f20be50..ad7a073 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -10,61 +10,108 @@ import {IHats} from "./../hatsprotocol/src/Interfaces/IHats.sol"; contract WorkingCapital is PluginCloneable { + struct MyAction { + address to; + uint256 value; + address ERC20; + } + IHats public hatsProtocolInstance; - uint256 public hatId; - uint256 public spendingLimitETH; + uint256 public hatId; + mapping(address => uint256) public spendingLimit; uint private currentMonth; uint private currentYear; - uint256 private remainingBudget; + mapping(address => uint256) private remainingBudget; + address[] availableTokens; /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId, uint256 _spendingLimitETH) external initializer { + function initialize(IDAO _dao, uint256 _hatId,address[] _token ,uint256[] _spendingLimit) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); - spendingLimitETH = _spendingLimitETH; + require(_token.length==_spendingLimit.length,"Length of token address and spendingLimit array is not equal"); + spendingLimit = _spendingLimit; + for (uint j=0; j < _token.length; j+=1) { + spendingLimit[_token[j]]=_spendingLimit[j]; + } + availableTokens=_token; + } + + + /// @notice Check that the given token has allowance + /// @param _token check that given token has allowance + function isTokenAvailable(address _token) internal pure returns (bool) { + for (uint256 i = 0; i < availableTokens.length; i++) { + if (availableTokens[i] == _token) { + return true; + } + } + return false; } + /// @notice Checking that can user withdraw this amount /// @param _actions actions that would be checked - function hasRemainingBudget(IDAO.Action[] calldata _actions) internal { + /// @return generatedDAOActions IDAO.Action generated for use in execute + function hasRemainingBudget(MyAction[] calldata _actions) internal returns(IDAO.Action[] generatedDAOActions){ uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); - uint j=0; - for (; j < _actions.length; j+=1) { //for loop example + for (uint j=0; j < _actions.length; j+=1) { + address _to; + uint256 _value; + bytes _data; + if(_actions[j].ERC20.length == 0){ + _to=_actions[j].to; + _value=_actions[j].value; + _data= bytes(0); + } + else{ + _to=__actions[j].ERC20; + _value=0; + bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); + _data= data; + } + require(isTokenAvailable(_token),"It is not available token in this plugin"); // if we are on the month that we were if(_currentMonth==currentMonth && _currentYear==currentYear){ require( - remainingBudget>=_actions[j].value, + remainingBudget[_token]>=_actions[j].value, string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") ); - remainingBudget-=_actions[j].value; + remainingBudget[_token] -=_actions[j].value; } + // if we are on another month else{ currentYear = _currentYear; currentMonth = _currentMonth; - remainingBudget=spendingLimitETH; + for (uint j=0; j < availableTokens.length; j+=1) { + remainingBudget[availableTokens[j]]=spendingLimit[availableTokens[j]]; + } require( - remainingBudget>=_actions[j].value, + remainingBudget[_token]>=_actions[j].value, string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") ); - remainingBudget-=_actions[j].value; + remainingBudget[_token]-=_actions[j].value; } + generatedDAOActions.push(IDAO.Action(_to, _value, _data)); + } } + //to value token + /// @notice Executes actions in the associated DAO. /// @param _actions The actions to be executed by the DAO. function execute( - IDAO.Action[] calldata _actions + MyAction[] calldata _myActions ) external { require(hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat"); - hasRemainingBudget(_actions); - dao().execute({_callId: 0x0, _actions: _actions, _allowFailureMap: 0}); + IDAO.Action [] idaoAction = hasRemainingBudget(_myActions); + dao().execute({_callId: 0x0, _actions: idaoAction, _allowFailureMap: 0}); } } diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index 6e0e103..b9a26ea 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -13,7 +13,8 @@ contract WorkingCapitalSetup is PluginSetup { struct InputData { uint256 hatId; - uint256 spendingLimitETH; + uint256[] spendingLimit; + address [] token; } /// @notice The address of `WorkingCapital` plugin logic contract to be cloned. @@ -39,7 +40,7 @@ contract WorkingCapitalSetup is PluginSetup { plugin = workingCapitalImplementation.clone(); // Initialize cloned plugin contract. - WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId, inputData.spendingLimitETH); + WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId, inputData.token,inputData.spendingLimit); // Prepare permissions PermissionLib.MultiTargetPermission[] From 258f3cdbd845e372a105ced0f7d181b4543292e7 Mon Sep 17 00:00:00 2001 From: Ali Khabazian <52274611+alikhabazian@users.noreply.github.com> Date: Thu, 12 Oct 2023 23:54:47 +0330 Subject: [PATCH 02/11] Update WorkingCapital.sol fix bugs about fixed size memory struct --- src/WorkingCapital.sol | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 0406635..86687d2 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -32,13 +32,13 @@ contract WorkingCapital is PluginCloneable { /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId,address[] _token ,uint256[] _spendingLimit) external initializer { + function initialize(IDAO _dao, uint256 _hatId,address[] memory _token ,uint256[] memory _spendingLimit) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); require(_token.length==_spendingLimit.length,"Length of token address and spendingLimit array is not equal"); - spendingLimit = _spendingLimit; +// spendingLimit = _spendingLimit; for (uint j=0; j < _token.length; j+=1) { spendingLimit[_token[j]]=_spendingLimit[j]; } @@ -48,7 +48,7 @@ contract WorkingCapital is PluginCloneable { /// @notice Check that the given token has allowance /// @param _token check that given token has allowance - function isTokenAvailable(address _token) internal pure returns (bool) { + function isTokenAvailable(address _token) internal view returns (bool) { for (uint256 i = 0; i < availableTokens.length; i++) { if (availableTokens[i] == _token) { return true; @@ -61,25 +61,32 @@ contract WorkingCapital is PluginCloneable { /// @notice Checking that can user withdraw this amount /// @param _actions actions that would be checked /// @return generatedDAOActions IDAO.Action generated for use in execute - function hasRemainingBudget(MyAction[] calldata _actions) internal returns(IDAO.Action[] generatedDAOActions){ + function hasRemainingBudget(MyAction[] calldata _actions) internal returns(IDAO.Action[] memory generatedDAOActions){ uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); + generatedDAOActions = new IDAO.Action[](_actions.length); for (uint j=0; j < _actions.length; j+=1) { address _to; uint256 _value; - bytes _data; - if(_actions[j].ERC20.length == 0){ + bytes memory _data; + address _token; + if(_actions[j].ERC20 == address(0)){ _to=_actions[j].to; _value=_actions[j].value; - _data= bytes(0); + _data= new bytes(0); + _token=address(0); + require(isTokenAvailable(address(0)),"It is not available token in this plugin"); + } else{ - _to=__actions[j].ERC20; + _to=_actions[j].ERC20; _value=0; bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); _data= data; + _token=_actions[j].ERC20; + require(isTokenAvailable(_actions[j].ERC20),"It is not available token in this plugin"); + } - require(isTokenAvailable(_token),"It is not available token in this plugin"); // if we are on the month that we were if(_currentMonth==currentMonth && _currentYear==currentYear){ require( @@ -102,25 +109,24 @@ contract WorkingCapital is PluginCloneable { remainingBudget[_token]-=_actions[j].value; } - generatedDAOActions.push(IDAO.Action(_to, _value, _data)); + generatedDAOActions[j]=IDAO.Action(_to, _value, _data); } } - //to value token /// @notice Executes actions in the associated DAO. - /// @param _actions The actions to be executed by the DAO. + /// @param _myActions The actions to be executed by the DAO. function execute( MyAction[] calldata _myActions ) external { require(hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat"); - IDAO.Action [] idaoAction = hasRemainingBudget(_myActions); + IDAO.Action [] memory idaoAction = hasRemainingBudget(_myActions); dao().execute({_callId: 0x0, _actions: idaoAction, _allowFailureMap: 0}); } - /// @param _spendingLimitETH The ETH spending limit - function updateSpendingLimit(uint256 _spendingLimitETH) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){ - spendingLimitETH = _spendingLimitETH; + /// @param _spendingLimit spending limit + function updateSpendingLimit(address _token,uint256 _spendingLimit) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){ + spendingLimit[_token] = _spendingLimit; } } From dd0203d69f5523b296aa0944d729edba215897fd Mon Sep 17 00:00:00 2001 From: Ali Khabazian <52274611+alikhabazian@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:02:14 +0330 Subject: [PATCH 03/11] Update src/WorkingCapital.sol change name of struct that store an action Co-authored-by: mehrdad mirmohammadsadeghi <35366864+mehrdadmms@users.noreply.github.com> --- src/WorkingCapital.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 86687d2..e132916 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -13,10 +13,10 @@ contract WorkingCapital is PluginCloneable { bytes32 public constant UPDATE_SPENDING_LIMIT_PERMISSION_ID = keccak256('UPDATE_SPENDING_LIMIT_PERMISSION'); - struct MyAction { + struct WorkingCapitalAction { address to; uint256 value; - address ERC20; + address erc20Address; } From 2a1a16d829dbeda3c30cc13a7e52ef7851a7351b Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Fri, 13 Oct 2023 16:12:57 +0330 Subject: [PATCH 04/11] Add Budget --- src/WorkingCapital.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index e132916..689f1b4 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -19,6 +19,11 @@ contract WorkingCapital is PluginCloneable { address erc20Address; } + struct Budget { + address token; + uint256 spendingLimit; + } + IHats public hatsProtocolInstance; uint256 public hatId; @@ -32,17 +37,15 @@ contract WorkingCapital is PluginCloneable { /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId,address[] memory _token ,uint256[] memory _spendingLimit) external initializer { + function initialize(IDAO _dao, uint256 _hatId,Budget[] memory calldata _budget) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); - require(_token.length==_spendingLimit.length,"Length of token address and spendingLimit array is not equal"); -// spendingLimit = _spendingLimit; for (uint j=0; j < _token.length; j+=1) { - spendingLimit[_token[j]]=_spendingLimit[j]; + spendingLimit[_budget[j].token] = _budget[j].spendingLimit; + availableTokens.push(_budget[j].token); } - availableTokens=_token; } From 5a8a224092d0031bd703c222e33df29d53e0c314 Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Fri, 13 Oct 2023 16:16:10 +0330 Subject: [PATCH 05/11] change execute --- src/WorkingCapital.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 689f1b4..14d4c2a 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -119,13 +119,13 @@ contract WorkingCapital is PluginCloneable { /// @notice Executes actions in the associated DAO. - /// @param _myActions The actions to be executed by the DAO. + /// @param _workingCapitalActions The actions to be executed by the DAO. function execute( - MyAction[] calldata _myActions + WorkingCapitalAction[] calldata _workingCapitalActions ) external { require(hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat"); - IDAO.Action [] memory idaoAction = hasRemainingBudget(_myActions); - dao().execute({_callId: 0x0, _actions: idaoAction, _allowFailureMap: 0}); + IDAO.Action [] memory iDAOAction = hasRemainingBudget(_workingCapitalActions); + dao().execute({_callId: 0x0, _actions: iDAOAction, _allowFailureMap: 0}); } /// @param _spendingLimit spending limit From d160cf4f09db0dc2d79cc982b2628aa8365a515e Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Fri, 13 Oct 2023 16:22:55 +0330 Subject: [PATCH 06/11] change hasRemainingBudget --- src/WorkingCapital.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 14d4c2a..d69276f 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -64,7 +64,7 @@ contract WorkingCapital is PluginCloneable { /// @notice Checking that can user withdraw this amount /// @param _actions actions that would be checked /// @return generatedDAOActions IDAO.Action generated for use in execute - function hasRemainingBudget(MyAction[] calldata _actions) internal returns(IDAO.Action[] memory generatedDAOActions){ + function hasRemainingBudget(WorkingCapitalAction[] calldata _actions) internal returns(IDAO.Action[] memory generatedDAOActions){ uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); generatedDAOActions = new IDAO.Action[](_actions.length); @@ -73,21 +73,20 @@ contract WorkingCapital is PluginCloneable { uint256 _value; bytes memory _data; address _token; - if(_actions[j].ERC20 == address(0)){ + if(_actions[j].erc20Address == address(0)){ _to=_actions[j].to; _value=_actions[j].value; _data= new bytes(0); _token=address(0); require(isTokenAvailable(address(0)),"It is not available token in this plugin"); - } else{ - _to=_actions[j].ERC20; + _to=_actions[j].erc20Address; _value=0; bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); _data= data; - _token=_actions[j].ERC20; - require(isTokenAvailable(_actions[j].ERC20),"It is not available token in this plugin"); + _token=_actions[j].erc20Address; + require(isTokenAvailable(_actions[j].erc20Address),"It is not available token in this plugin"); } // if we are on the month that we were From b8a91b4b8cc451010e2059d3221db19a58391ae5 Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Fri, 13 Oct 2023 16:29:26 +0330 Subject: [PATCH 07/11] add budget --- src/WorkingCapitalSetup.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index 8242aa5..f336930 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -8,13 +8,19 @@ import {WorkingCapital} from "./WorkingCapital.sol"; import {IDAO} from "@aragon/osx/core/plugin/Plugin.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; + contract WorkingCapitalSetup is PluginSetup { using Clones for address; + struct Budget { + address token; + uint256 spendingLimit; + } + struct InputData { uint256 hatId; - uint256[] spendingLimit; - address [] token; + Budget[] budget; + } /// @notice The address of `WorkingCapital` plugin logic contract to be cloned. @@ -40,7 +46,7 @@ contract WorkingCapitalSetup is PluginSetup { plugin = workingCapitalImplementation.clone(); // Initialize cloned plugin contract. - WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId, inputData.token,inputData.spendingLimit); + WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId,inputData.budget); // Prepare permissions PermissionLib.MultiTargetPermission[] From 1b28a3d320908fe202a6636d7afc3b0f220f233d Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Fri, 13 Oct 2023 18:58:59 +0330 Subject: [PATCH 08/11] resolve some bugs --- src/WorkingCapital.sol | 27 ++++++++++----------------- src/WorkingCapitalSetup.sol | 7 +------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index d69276f..505c464 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -32,35 +32,28 @@ contract WorkingCapital is PluginCloneable { uint private currentMonth; uint private currentYear; mapping(address => uint256) private remainingBudget; - address[] availableTokens; + address[] public availableTokens; + + + + + /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId,Budget[] memory calldata _budget) external initializer { + function initialize(IDAO _dao, uint256 _hatId,Budget[] calldata _budget) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); - for (uint j=0; j < _token.length; j+=1) { + for (uint j=0; j < _budget.length; j+=1) { spendingLimit[_budget[j].token] = _budget[j].spendingLimit; availableTokens.push(_budget[j].token); } } - /// @notice Check that the given token has allowance - /// @param _token check that given token has allowance - function isTokenAvailable(address _token) internal view returns (bool) { - for (uint256 i = 0; i < availableTokens.length; i++) { - if (availableTokens[i] == _token) { - return true; - } - } - return false; - } - - /// @notice Checking that can user withdraw this amount /// @param _actions actions that would be checked /// @return generatedDAOActions IDAO.Action generated for use in execute @@ -78,7 +71,7 @@ contract WorkingCapital is PluginCloneable { _value=_actions[j].value; _data= new bytes(0); _token=address(0); - require(isTokenAvailable(address(0)),"It is not available token in this plugin"); + require(spendingLimit[address(0x0)] != 0,"It is not available token in this plugin"); } else{ _to=_actions[j].erc20Address; @@ -86,7 +79,7 @@ contract WorkingCapital is PluginCloneable { bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); _data= data; _token=_actions[j].erc20Address; - require(isTokenAvailable(_actions[j].erc20Address),"It is not available token in this plugin"); + require(spendingLimit[_actions[j].erc20Address]!=0,"It is not available token in this plugin"); } // if we are on the month that we were diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index f336930..a10d7d8 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -12,15 +12,10 @@ import {DAO} from "@aragon/osx/core/dao/DAO.sol"; contract WorkingCapitalSetup is PluginSetup { using Clones for address; - struct Budget { - address token; - uint256 spendingLimit; - } struct InputData { uint256 hatId; - Budget[] budget; - + WorkingCapital.Budget[] budget; } /// @notice The address of `WorkingCapital` plugin logic contract to be cloned. From 2108185997d06f09336c7ba74e6b7fec06067b5d Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Sat, 14 Oct 2023 17:01:35 +0330 Subject: [PATCH 09/11] delete array --- plugin-settings.ts | 2 +- src/WorkingCapital.sol | 72 +++++++++++++++++++++++-------------- src/WorkingCapitalSetup.sol | 6 ++-- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/plugin-settings.ts b/plugin-settings.ts index 1b30673..bdc6d82 100644 --- a/plugin-settings.ts +++ b/plugin-settings.ts @@ -1,7 +1,7 @@ import buildMetadata from './src/build-metadata.json'; import releaseMetadata from './src/release-metadata.json'; -export const PLUGIN_REPO_ENS_NAME = 'wc-test-0318911'; +export const PLUGIN_REPO_ENS_NAME = `ws-test-${new Date().getTime()}`; export const PLUGIN_CONTRACT_NAME = 'WorkingCapital'; export const PLUGIN_SETUP_CONTRACT_NAME = 'WorkingCapitalSetup'; diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 505c464..72439eb 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -19,20 +19,30 @@ contract WorkingCapital is PluginCloneable { address erc20Address; } - struct Budget { - address token; +// struct Budget { +// address token; +// uint256 spendingLimit; +// } + + + struct TokenDetails{ + uint lastMonthEdit; + uint lastYearEdit; uint256 spendingLimit; + uint256 remainingBudget; } + mapping(address => TokenDetails) public budgets; + IHats public hatsProtocolInstance; uint256 public hatId; - mapping(address => uint256) public spendingLimit; +// mapping(address => uint256) public spendingLimit; - uint private currentMonth; - uint private currentYear; - mapping(address => uint256) private remainingBudget; - address[] public availableTokens; +// uint private currentMonth; +// uint private currentYear; +// mapping(address => uint256) private remainingBudget; +// address[] public availableTokens; @@ -42,15 +52,18 @@ contract WorkingCapital is PluginCloneable { /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId,Budget[] calldata _budget) external initializer { + function initialize(IDAO _dao, uint256 _hatId,uint256 _budgetETH) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); - for (uint j=0; j < _budget.length; j+=1) { - spendingLimit[_budget[j].token] = _budget[j].spendingLimit; - availableTokens.push(_budget[j].token); + if (_budgetETH>0){ + uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); + uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); + budgets[address (0)] = TokenDetails({lastMonthEdit:_currentMonth, lastYearEdit:_currentYear,spendingLimit:_budgetETH,remainingBudget:_budgetETH}); +// availableTokens.push(address (0)); } + } @@ -66,42 +79,40 @@ contract WorkingCapital is PluginCloneable { uint256 _value; bytes memory _data; address _token; + // it is not an erc20 token if(_actions[j].erc20Address == address(0)){ _to=_actions[j].to; _value=_actions[j].value; _data= new bytes(0); _token=address(0); - require(spendingLimit[address(0x0)] != 0,"It is not available token in this plugin"); + require(budgets[_token].spendingLimit != 0,"It is not available token in this plugin"); } else{ _to=_actions[j].erc20Address; _value=0; - bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); - _data= data; + _data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); _token=_actions[j].erc20Address; - require(spendingLimit[_actions[j].erc20Address]!=0,"It is not available token in this plugin"); + require(budgets[_token].spendingLimit !=0,"It is not available token in this plugin"); } // if we are on the month that we were - if(_currentMonth==currentMonth && _currentYear==currentYear){ + if(_currentMonth==budgets[_token].lastMonthEdit && _currentYear==budgets[_token].lastYearEdit){ require( - remainingBudget[_token]>=_actions[j].value, + budgets[_token].remainingBudget >=_actions[j].value, string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") ); - remainingBudget[_token] -=_actions[j].value; + budgets[_token].remainingBudget -=_actions[j].value; } // if we are on another month else{ - currentYear = _currentYear; - currentMonth = _currentMonth; - for (uint j=0; j < availableTokens.length; j+=1) { - remainingBudget[availableTokens[j]]=spendingLimit[availableTokens[j]]; - } + budgets[_token].lastYearEdit = _currentYear; + budgets[_token].lastMonthEdit = _currentMonth; + budgets[_token].remainingBudget = budgets[_token].spendingLimit; require( - remainingBudget[_token]>=_actions[j].value, + budgets[_token].remainingBudget>=_actions[j].value, string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") ); - remainingBudget[_token]-=_actions[j].value; + budgets[_token].remainingBudget-=_actions[j].value; } generatedDAOActions[j]=IDAO.Action(_to, _value, _data); @@ -121,7 +132,14 @@ contract WorkingCapital is PluginCloneable { } /// @param _spendingLimit spending limit - function updateSpendingLimit(address _token,uint256 _spendingLimit) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){ - spendingLimit[_token] = _spendingLimit; + function updateSpendingLimit(address _token,uint256 _spendingLimit,bool _restThisMonth) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){ + uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); + uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); + if(_restThisMonth){ + budgets[_token]=TokenDetails({lastMonthEdit:_currentMonth,lastYearEdit:_currentYear,spendingLimit:_spendingLimit,remainingBudget:_spendingLimit}); + } + else{ + budgets[_token]=TokenDetails({lastMonthEdit:_currentMonth,lastYearEdit:_currentYear,spendingLimit:_spendingLimit,remainingBudget:budgets[_token].remainingBudget}); + } } } diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index a10d7d8..bc9bebf 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -14,8 +14,8 @@ contract WorkingCapitalSetup is PluginSetup { struct InputData { + uint256 budgetETH; uint256 hatId; - WorkingCapital.Budget[] budget; } /// @notice The address of `WorkingCapital` plugin logic contract to be cloned. @@ -35,13 +35,15 @@ contract WorkingCapitalSetup is PluginSetup { returns (address plugin, PreparedSetupData memory preparedSetupData) { // Decode `_data` to extract the params needed for cloning and initializing the `Admin` plugin. + + InputData memory inputData = abi.decode(_data, (InputData)); // Clone plugin contract. plugin = workingCapitalImplementation.clone(); // Initialize cloned plugin contract. - WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId,inputData.budget); + WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId,inputData.budgetETH); // Prepare permissions PermissionLib.MultiTargetPermission[] From 5e198c11689e7fb330768dc21b686c7a2507e497 Mon Sep 17 00:00:00 2001 From: mehrdad Date: Sun, 15 Oct 2023 20:54:55 +0330 Subject: [PATCH 10/11] prittified the code --- src/WorkingCapital.sol | 184 ++++++++++++++++++++++-------------- src/WorkingCapitalSetup.sol | 17 ++-- 2 files changed, 123 insertions(+), 78 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index 72439eb..b878406 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -9,9 +9,8 @@ import {BokkyPooBahsDateTimeLibrary} from "./BokkyPooBahsDateTimeLibrary.sol"; import {IHats} from "./../hatsprotocol/src/Interfaces/IHats.sol"; contract WorkingCapital is PluginCloneable { - - - bytes32 public constant UPDATE_SPENDING_LIMIT_PERMISSION_ID = keccak256('UPDATE_SPENDING_LIMIT_PERMISSION'); + bytes32 public constant UPDATE_SPENDING_LIMIT_PERMISSION_ID = + keccak256("UPDATE_SPENDING_LIMIT_PERMISSION"); struct WorkingCapitalAction { address to; @@ -19,13 +18,7 @@ contract WorkingCapital is PluginCloneable { address erc20Address; } -// struct Budget { -// address token; -// uint256 spendingLimit; -// } - - - struct TokenDetails{ + struct TokenDetails { uint lastMonthEdit; uint lastYearEdit; uint256 spendingLimit; @@ -34,112 +27,161 @@ contract WorkingCapital is PluginCloneable { mapping(address => TokenDetails) public budgets; - IHats public hatsProtocolInstance; uint256 public hatId; -// mapping(address => uint256) public spendingLimit; - -// uint private currentMonth; -// uint private currentYear; -// mapping(address => uint256) private remainingBudget; -// address[] public availableTokens; - - - - - /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. - function initialize(IDAO _dao, uint256 _hatId,uint256 _budgetETH) external initializer { + function initialize( + IDAO _dao, + uint256 _hatId, + uint256 _budgetETH + ) external initializer { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) - hatsProtocolInstance = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); - if (_budgetETH>0){ - uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); - uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); - budgets[address (0)] = TokenDetails({lastMonthEdit:_currentMonth, lastYearEdit:_currentYear,spendingLimit:_budgetETH,remainingBudget:_budgetETH}); -// availableTokens.push(address (0)); + hatsProtocolInstance = IHats( + 0x3bc1A0Ad72417f2d411118085256fC53CBdDd137 + ); + if (_budgetETH > 0) { + uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( + block.timestamp + ); + uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( + block.timestamp + ); + budgets[address(0)] = TokenDetails({ + lastMonthEdit: _currentMonth, + lastYearEdit: _currentYear, + spendingLimit: _budgetETH, + remainingBudget: _budgetETH + }); } - } - /// @notice Checking that can user withdraw this amount /// @param _actions actions that would be checked /// @return generatedDAOActions IDAO.Action generated for use in execute - function hasRemainingBudget(WorkingCapitalAction[] calldata _actions) internal returns(IDAO.Action[] memory generatedDAOActions){ - uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); - uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); + function hasRemainingBudget( + WorkingCapitalAction[] calldata _actions + ) internal returns (IDAO.Action[] memory generatedDAOActions) { + uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( + block.timestamp + ); + uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( + block.timestamp + ); generatedDAOActions = new IDAO.Action[](_actions.length); - for (uint j=0; j < _actions.length; j+=1) { + for (uint j = 0; j < _actions.length; j += 1) { address _to; uint256 _value; bytes memory _data; address _token; // it is not an erc20 token - if(_actions[j].erc20Address == address(0)){ - _to=_actions[j].to; - _value=_actions[j].value; - _data= new bytes(0); - _token=address(0); - require(budgets[_token].spendingLimit != 0,"It is not available token in this plugin"); - } - else{ - _to=_actions[j].erc20Address; - _value=0; - _data = abi.encodeWithSignature("transfer(address,uint256)", _actions[j].to, _actions[j].value); - _token=_actions[j].erc20Address; - require(budgets[_token].spendingLimit !=0,"It is not available token in this plugin"); - + if (_actions[j].erc20Address == address(0)) { + _to = _actions[j].to; + _value = _actions[j].value; + _data = new bytes(0); + _token = address(0); + require( + budgets[_token].spendingLimit != 0, + "It is not available token in this plugin" + ); + } else { + _to = _actions[j].erc20Address; + _value = 0; + _data = abi.encodeWithSignature( + "transfer(address,uint256)", + _actions[j].to, + _actions[j].value + ); + _token = _actions[j].erc20Address; + require( + budgets[_token].spendingLimit != 0, + "It is not available token in this plugin" + ); } // if we are on the month that we were - if(_currentMonth==budgets[_token].lastMonthEdit && _currentYear==budgets[_token].lastYearEdit){ + if ( + _currentMonth == budgets[_token].lastMonthEdit && + _currentYear == budgets[_token].lastYearEdit + ) { require( - budgets[_token].remainingBudget >=_actions[j].value, - string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") + budgets[_token].remainingBudget >= _actions[j].value, + string.concat( + "In ", + Strings.toString(j), + " action you want to spend more than your limit monthly" + ) ); - budgets[_token].remainingBudget -=_actions[j].value; + budgets[_token].remainingBudget -= _actions[j].value; } // if we are on another month - else{ + else { budgets[_token].lastYearEdit = _currentYear; budgets[_token].lastMonthEdit = _currentMonth; budgets[_token].remainingBudget = budgets[_token].spendingLimit; require( - budgets[_token].remainingBudget>=_actions[j].value, - string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly") + budgets[_token].remainingBudget >= _actions[j].value, + string.concat( + "In ", + Strings.toString(j), + " action you want to spend more than your limit monthly" + ) ); - budgets[_token].remainingBudget-=_actions[j].value; + budgets[_token].remainingBudget -= _actions[j].value; } - generatedDAOActions[j]=IDAO.Action(_to, _value, _data); - + generatedDAOActions[j] = IDAO.Action(_to, _value, _data); } } - /// @notice Executes actions in the associated DAO. /// @param _workingCapitalActions The actions to be executed by the DAO. function execute( WorkingCapitalAction[] calldata _workingCapitalActions ) external { - require(hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat"); - IDAO.Action [] memory iDAOAction = hasRemainingBudget(_workingCapitalActions); - dao().execute({_callId: 0x0, _actions: iDAOAction, _allowFailureMap: 0}); + require( + hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), + "Sender is not wearer of the hat" + ); + IDAO.Action[] memory iDAOAction = hasRemainingBudget( + _workingCapitalActions + ); + dao().execute({ + _callId: 0x0, + _actions: iDAOAction, + _allowFailureMap: 0 + }); } /// @param _spendingLimit spending limit - function updateSpendingLimit(address _token,uint256 _spendingLimit,bool _restThisMonth) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){ - uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp); - uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp); - if(_restThisMonth){ - budgets[_token]=TokenDetails({lastMonthEdit:_currentMonth,lastYearEdit:_currentYear,spendingLimit:_spendingLimit,remainingBudget:_spendingLimit}); - } - else{ - budgets[_token]=TokenDetails({lastMonthEdit:_currentMonth,lastYearEdit:_currentYear,spendingLimit:_spendingLimit,remainingBudget:budgets[_token].remainingBudget}); + function updateSpendingLimit( + address _token, + uint256 _spendingLimit, + bool _restThisMonth + ) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID) { + uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( + block.timestamp + ); + uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( + block.timestamp + ); + if (_restThisMonth) { + budgets[_token] = TokenDetails({ + lastMonthEdit: _currentMonth, + lastYearEdit: _currentYear, + spendingLimit: _spendingLimit, + remainingBudget: _spendingLimit + }); + } else { + budgets[_token] = TokenDetails({ + lastMonthEdit: _currentMonth, + lastYearEdit: _currentYear, + spendingLimit: _spendingLimit, + remainingBudget: budgets[_token].remainingBudget + }); } } } diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index bc9bebf..f59debe 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -8,16 +8,14 @@ import {WorkingCapital} from "./WorkingCapital.sol"; import {IDAO} from "@aragon/osx/core/plugin/Plugin.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; - contract WorkingCapitalSetup is PluginSetup { using Clones for address; - struct InputData { uint256 budgetETH; uint256 hatId; } - + /// @notice The address of `WorkingCapital` plugin logic contract to be cloned. address private immutable workingCapitalImplementation; @@ -36,14 +34,17 @@ contract WorkingCapitalSetup is PluginSetup { { // Decode `_data` to extract the params needed for cloning and initializing the `Admin` plugin. - InputData memory inputData = abi.decode(_data, (InputData)); // Clone plugin contract. plugin = workingCapitalImplementation.clone(); // Initialize cloned plugin contract. - WorkingCapital(plugin).initialize(IDAO(_dao), inputData.hatId,inputData.budgetETH); + WorkingCapital(plugin).initialize( + IDAO(_dao), + inputData.hatId, + inputData.budgetETH + ); // Prepare permissions PermissionLib.MultiTargetPermission[] @@ -63,7 +64,8 @@ contract WorkingCapitalSetup is PluginSetup { where: plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: WorkingCapital(plugin).UPDATE_SPENDING_LIMIT_PERMISSION_ID() + permissionId: WorkingCapital(plugin) + .UPDATE_SPENDING_LIMIT_PERMISSION_ID() }); preparedSetupData.permissions = permissions; @@ -97,7 +99,8 @@ contract WorkingCapitalSetup is PluginSetup { where: plugin, who: _dao, condition: PermissionLib.NO_CONDITION, - permissionId: WorkingCapital(plugin).UPDATE_SPENDING_LIMIT_PERMISSION_ID() + permissionId: WorkingCapital(plugin) + .UPDATE_SPENDING_LIMIT_PERMISSION_ID() }); } From 336e1ab1f002f03233c06c106e3a183b611091c2 Mon Sep 17 00:00:00 2001 From: alikhabazian Date: Sun, 15 Oct 2023 22:24:21 +0330 Subject: [PATCH 11/11] Adding comments to contracts --- src/WorkingCapital.sol | 50 +++++++++++++++++++++++++++++-------- src/WorkingCapitalSetup.sol | 2 +- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/WorkingCapital.sol b/src/WorkingCapital.sol index b878406..974c729 100644 --- a/src/WorkingCapital.sol +++ b/src/WorkingCapital.sol @@ -33,6 +33,7 @@ contract WorkingCapital is PluginCloneable { /// @notice Initializes the contract. /// @param _dao The associated DAO. /// @param _hatId The id of the hat. + /// @param _budgetETH the limit of budget in ETH function initialize( IDAO _dao, uint256 _hatId, @@ -41,16 +42,21 @@ contract WorkingCapital is PluginCloneable { __PluginCloneable_init(_dao); hatId = _hatId; // TODO get this from environment per network (this is goerli) + // get instance of Hats protocol hatsProtocolInstance = IHats( 0x3bc1A0Ad72417f2d411118085256fC53CBdDd137 ); + // can hats owner spend any ETH if (_budgetETH > 0) { + // get current month from timestamp uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( block.timestamp ); + // get current year from timestamp uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( block.timestamp ); + // add limitation of ETH ,remainingBudget ,currentMonth and currentYear in budgets map in address(0) budgets[address(0)] = TokenDetails({ lastMonthEdit: _currentMonth, lastYearEdit: _currentYear, @@ -66,12 +72,15 @@ contract WorkingCapital is PluginCloneable { function hasRemainingBudget( WorkingCapitalAction[] calldata _actions ) internal returns (IDAO.Action[] memory generatedDAOActions) { + // get current month from timestamp uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( block.timestamp ); + // get current year from timestamp uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( block.timestamp ); + // create generated Dao action that includes consistent actions with dao().execute() generatedDAOActions = new IDAO.Action[](_actions.length); for (uint j = 0; j < _actions.length; j += 1) { address _to; @@ -80,33 +89,40 @@ contract WorkingCapital is PluginCloneable { address _token; // it is not an erc20 token if (_actions[j].erc20Address == address(0)) { + // which token has been spent + _token = address(0); + require( + budgets[_token].spendingLimit != 0, + "It is not available token in this plugin" + ); _to = _actions[j].to; _value = _actions[j].value; _data = new bytes(0); - _token = address(0); + + } else { + // which token has been spent + _token = _actions[j].erc20Address; require( budgets[_token].spendingLimit != 0, "It is not available token in this plugin" ); - } else { + // address of token that we must call _to = _actions[j].erc20Address; _value = 0; + // encode transaction that dao().execute() must run _data = abi.encodeWithSignature( "transfer(address,uint256)", _actions[j].to, _actions[j].value ); - _token = _actions[j].erc20Address; - require( - budgets[_token].spendingLimit != 0, - "It is not available token in this plugin" - ); + } - // if we are on the month that we were + // if we are on the month that we were in last modification of this token if ( _currentMonth == budgets[_token].lastMonthEdit && _currentYear == budgets[_token].lastYearEdit ) { + // check that we have enough remaining budget require( budgets[_token].remainingBudget >= _actions[j].value, string.concat( @@ -115,12 +131,15 @@ contract WorkingCapital is PluginCloneable { " action you want to spend more than your limit monthly" ) ); + // reduce from this token budget of hats owner(s) in this month budgets[_token].remainingBudget -= _actions[j].value; } - // if we are on another month + // if we are on another month after modification this token else { + // update token lastYearEdit and lastMonthEdit budgets[_token].lastYearEdit = _currentYear; budgets[_token].lastMonthEdit = _currentMonth; + // reset this month budget budgets[_token].remainingBudget = budgets[_token].spendingLimit; require( budgets[_token].remainingBudget >= _actions[j].value, @@ -130,9 +149,10 @@ contract WorkingCapital is PluginCloneable { " action you want to spend more than your limit monthly" ) ); + // reduce from this token budget of hats owner(s) in this month budgets[_token].remainingBudget -= _actions[j].value; } - + // add this new action to generatedDAOActions to call them together generatedDAOActions[j] = IDAO.Action(_to, _value, _data); } } @@ -142,13 +162,16 @@ contract WorkingCapital is PluginCloneable { function execute( WorkingCapitalAction[] calldata _workingCapitalActions ) external { + // check that caller of this transaction is hats owner(s) require( hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat" ); + // get generated actions IDAO.Action[] memory iDAOAction = hasRemainingBudget( _workingCapitalActions ); + // execute generated actions dao().execute({ _callId: 0x0, _actions: iDAOAction, @@ -156,18 +179,24 @@ contract WorkingCapital is PluginCloneable { }); } + /// @notice UpdateSpendingLimit is an function that just would call with dao and must create proposal for that + /// @param _token which token budget you want to modify /// @param _spendingLimit spending limit + /// @param _restThisMonth reset remaining budget of this month or not function updateSpendingLimit( address _token, uint256 _spendingLimit, bool _restThisMonth ) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID) { + // get current month from timestamp uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth( block.timestamp ); + // get current year from timestamp uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear( block.timestamp ); + // to reset budget of this token if (_restThisMonth) { budgets[_token] = TokenDetails({ lastMonthEdit: _currentMonth, @@ -176,6 +205,7 @@ contract WorkingCapital is PluginCloneable { remainingBudget: _spendingLimit }); } else { + // remainingBudget will not update budgets[_token] = TokenDetails({ lastMonthEdit: _currentMonth, lastYearEdit: _currentYear, diff --git a/src/WorkingCapitalSetup.sol b/src/WorkingCapitalSetup.sol index f59debe..259bda9 100644 --- a/src/WorkingCapitalSetup.sol +++ b/src/WorkingCapitalSetup.sol @@ -58,7 +58,7 @@ contract WorkingCapitalSetup is PluginSetup { condition: PermissionLib.NO_CONDITION, permissionId: DAO(payable(_dao)).EXECUTE_PERMISSION_ID() }); - + // Grant the `UPDATE_SPENDING_LIMIT_PERMISSION_ID` on the plugin to the DAO. permissions[1] = PermissionLib.MultiTargetPermission({ operation: PermissionLib.Operation.Grant, where: plugin,