Skip to content
2 changes: 1 addition & 1 deletion plugin-settings.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
212 changes: 176 additions & 36 deletions src/WorkingCapital.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,209 @@ 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;
uint256 value;
address erc20Address;
}

IHats public hatsProtocolInstance;
uint256 public hatId;
uint256 public spendingLimitETH;
struct TokenDetails {
uint lastMonthEdit;
uint lastYearEdit;
uint256 spendingLimit;
uint256 remainingBudget;
}

mapping(address => TokenDetails) public budgets;

uint private currentMonth;
uint private currentYear;
uint256 private remainingBudget;
IHats public hatsProtocolInstance;
uint256 public hatId;

/// @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 {
/// @param _budgetETH the limit of budget in ETH
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);
spendingLimitETH = _spendingLimitETH;
// 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,
spendingLimit: _budgetETH,
remainingBudget: _budgetETH
});
}
}

/// @notice Checking that can user withdraw this amount
/// @param _actions actions that would be checked
function hasRemainingBudget(IDAO.Action[] calldata _actions) internal {
uint _currentMonth = BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp);
uint _currentYear = BokkyPooBahsDateTimeLibrary.getYear(block.timestamp);
uint j=0;
for (; j < _actions.length; j+=1) { //for loop example
// if we are on the month that we were
if(_currentMonth==currentMonth && _currentYear==currentYear){
/// @return generatedDAOActions IDAO.Action generated for use in execute
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;
uint256 _value;
bytes memory _data;
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);

} else {
// which token has been spent
_token = _actions[j].erc20Address;
require(
remainingBudget>=_actions[j].value,
string.concat("In ",Strings.toString(j)," action you want to spend more than your limit monthly")
budgets[_token].spendingLimit != 0,
"It is not available token in this plugin"
);
// 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
);
remainingBudget-=_actions[j].value;

}
else{
currentYear = _currentYear;
currentMonth = _currentMonth;
remainingBudget=spendingLimitETH;
// 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(
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"
)
);
remainingBudget-=_actions[j].value;
// reduce from this token budget of hats owner(s) in this month
budgets[_token].remainingBudget -= _actions[j].value;
}

// 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,
string.concat(
"In ",
Strings.toString(j),
" 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);
}
}

/// @notice Executes actions in the associated DAO.
/// @param _actions The actions to be executed by the DAO.
/// @param _workingCapitalActions The actions to be executed by the DAO.
function execute(
IDAO.Action[] calldata _actions
WorkingCapitalAction[] calldata _workingCapitalActions
) external {
require(hatsProtocolInstance.isWearerOfHat(msg.sender, hatId), "Sender is not wearer of the hat");
hasRemainingBudget(_actions);
dao().execute({_callId: 0x0, _actions: _actions, _allowFailureMap: 0});
// 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,
_allowFailureMap: 0
});
}

/// @param _spendingLimitETH The ETH spending limit
function updateSpendingLimit(uint256 _spendingLimitETH) external auth(UPDATE_SPENDING_LIMIT_PERMISSION_ID){
spendingLimitETH = _spendingLimitETH;
/// @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,
lastYearEdit: _currentYear,
spendingLimit: _spendingLimit,
remainingBudget: _spendingLimit
});
} else {
// remainingBudget will not update
budgets[_token] = TokenDetails({
lastMonthEdit: _currentMonth,
lastYearEdit: _currentYear,
spendingLimit: _spendingLimit,
remainingBudget: budgets[_token].remainingBudget
});
}
}
}
19 changes: 13 additions & 6 deletions src/WorkingCapitalSetup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ contract WorkingCapitalSetup is PluginSetup {
using Clones for address;

struct InputData {
uint256 budgetETH;
uint256 hatId;
uint256 spendingLimitETH;
}

/// @notice The address of `WorkingCapital` plugin logic contract to be cloned.
address private immutable workingCapitalImplementation;

Expand All @@ -33,13 +33,18 @@ 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.spendingLimitETH);
WorkingCapital(plugin).initialize(
IDAO(_dao),
inputData.hatId,
inputData.budgetETH
);

// Prepare permissions
PermissionLib.MultiTargetPermission[]
Expand All @@ -53,13 +58,14 @@ 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,
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;
Expand Down Expand Up @@ -93,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()
});
}

Expand Down