Skip to content

Commit 19682c2

Browse files
GenericSchemeMultiCall : Simplification. + Schemeconstraint (#795)
* simplify contract * tests * test approval * check spender is white listed * clean * poc * Add SchemeConstraints interface * add eth constraint as an example for DxDaoSchemeConstraint * + * more .. * fix dxdao constraints * check schemeConstraint exist * + comments * coverage * remove unused var * more tests * Bump version Co-authored-by: benk10 <[email protected]>
1 parent 293637e commit 19682c2

File tree

6 files changed

+406
-141
lines changed

6 files changed

+406
-141
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
pragma solidity 0.5.17;
2+
pragma experimental ABIEncoderV2;
3+
4+
import "./SchemeConstraints.sol";
5+
6+
7+
contract DxDaoSchemeConstraints is SchemeConstraints {
8+
using SafeMath for uint256;
9+
10+
uint256 public initialTimestamp;
11+
uint256 public periodSize;
12+
uint256 public periodLimitWei;
13+
14+
mapping(address=>uint256) public periodLimitToken;
15+
mapping (uint256 => mapping(address => uint256)) public periodSpendingToken;
16+
mapping(uint256=>uint256) public periodSpendingWei;
17+
mapping(address=>bool) public contractsWhiteListMap;
18+
bytes4 private constant APPROVE_SIGNATURE = 0x095ea7b3;//approve(address,uint256)
19+
20+
/* @dev initialize
21+
* @param _periodSize the time period to limit the tokens and eth spending
22+
* @param _periodLimitWei the limit of eth which can be sent per period
23+
* @param _periodLimitTokensAddresses tokens to limit
24+
* @param _periodLimitTokensAmounts the limit of token which can be sent per period
25+
* @param _contractsWhiteList the contracts the scheme is allowed to interact with
26+
*/
27+
function initialize(
28+
uint256 _periodSize,
29+
uint256 _periodLimitWei,
30+
address[] calldata _periodLimitTokensAddresses,
31+
uint256[] calldata _periodLimitTokensAmounts,
32+
address[] calldata _contractsWhiteList
33+
)
34+
external {
35+
require(initialTimestamp == 0, "cannot initialize twice");
36+
require(_periodSize > 0, "preriod size should be greater than 0");
37+
require(_periodLimitTokensAddresses.length == _periodLimitTokensAmounts.length,
38+
"invalid length _periodLimitTokensAddresses");
39+
periodSize = _periodSize;
40+
periodLimitWei = _periodLimitWei;
41+
// solhint-disable-next-line not-rely-on-time
42+
initialTimestamp = block.timestamp;
43+
for (uint i = 0; i < _contractsWhiteList.length; i++) {
44+
contractsWhiteListMap[_contractsWhiteList[i]] = true;
45+
}
46+
for (uint i = 0; i < _periodLimitTokensAmounts.length; i++) {
47+
periodLimitToken[_periodLimitTokensAddresses[i]] = _periodLimitTokensAmounts[i];
48+
}
49+
contractsWhiteList = _contractsWhiteList;
50+
}
51+
52+
/*
53+
* @dev isAllowedToCall should be called upon a proposal execution.
54+
* - check that the total spending of tokens within a 'periodSize' does not exceed the periodLimit per token
55+
* - check that the total sending of eth within a 'periodSize' does not exceed the periodLimit
56+
* @param _contractsToCall the contracts to be called
57+
* @param _callsData - The abi encode data for the calls
58+
* @param _values value(ETH) to transfer with the calls
59+
* @param _avatar avatar
60+
* @return bool value true-allowed false not allowed
61+
*/
62+
function isAllowedToCall(
63+
address[] calldata _contractsToCall,
64+
bytes[] calldata _callsData,
65+
uint256[] calldata _values,
66+
Avatar
67+
)
68+
external
69+
returns(bool)
70+
{
71+
72+
uint256 observervationIndex = observationIndex();
73+
uint256 totalPeriodSpendingInWei;
74+
for (uint i = 0; i < _contractsToCall.length; i++) {
75+
// constraint eth transfer
76+
totalPeriodSpendingInWei = totalPeriodSpendingInWei.add(_values[i]);
77+
bytes memory callData = _callsData[i];
78+
// constraint approve calls
79+
if (callData[0] == APPROVE_SIGNATURE[0] &&
80+
callData[1] == APPROVE_SIGNATURE[1] &&
81+
callData[2] == APPROVE_SIGNATURE[2] &&
82+
callData[3] == APPROVE_SIGNATURE[3]) {
83+
uint256 amount;
84+
address contractToCall = _contractsToCall[i];
85+
// solhint-disable-next-line no-inline-assembly
86+
assembly {
87+
amount := mload(add(callData, 68))
88+
}
89+
periodSpendingToken[observervationIndex][contractToCall] =
90+
periodSpendingToken[observervationIndex][contractToCall].add(amount);
91+
require(
92+
periodSpendingToken[observervationIndex][contractToCall] <= periodLimitToken[contractToCall],
93+
"periodSpendingTokensExceeded");
94+
}
95+
96+
}
97+
periodSpendingWei[observervationIndex] =
98+
periodSpendingWei[observervationIndex].add(totalPeriodSpendingInWei);
99+
require(periodSpendingWei[observervationIndex] <= periodLimitWei, "periodSpendingWeiExceeded");
100+
return true;
101+
}
102+
103+
/*
104+
* @dev isAllowedToPropose should be called upon a proposal submition.
105+
* allow only whitelisted target contracts or 'approve' calls which the 'spender' is whitelisted
106+
* @param _contractsToCall the contracts to be called
107+
* @param _callsData - The abi encode data for the calls
108+
* @param _values value(ETH) to transfer with the calls
109+
* @param _avatar avatar
110+
* @return bool value true-allowed false not allowed
111+
*/
112+
function isAllowedToPropose(
113+
address[] calldata _contractsToCall,
114+
bytes[] calldata _callsData,
115+
uint256[] calldata,
116+
Avatar)
117+
external
118+
returns(bool)
119+
{
120+
for (uint i = 0; i < _contractsToCall.length; i++) {
121+
if (!contractsWhiteListMap[_contractsToCall[i]]) {
122+
address spender;
123+
bytes memory callData = _callsData[i];
124+
require(
125+
callData[0] == APPROVE_SIGNATURE[0] &&
126+
callData[1] == APPROVE_SIGNATURE[1] &&
127+
callData[2] == APPROVE_SIGNATURE[2] &&
128+
callData[3] == APPROVE_SIGNATURE[3],
129+
"allow only approve call for none whitelistedContracts");
130+
//in solidity > 6 this can be replaced by:
131+
//(spender,) = abi.descode(callData[4:], (address, uint));
132+
// see https://github.com/ethereum/solidity/issues/9439
133+
// solhint-disable-next-line no-inline-assembly
134+
assembly {
135+
spender := mload(add(callData, 36))
136+
}
137+
require(contractsWhiteListMap[spender], "spender contract not whitelisted");
138+
}
139+
}
140+
return true;
141+
}
142+
143+
function observationIndex() public view returns (uint256) {
144+
// solhint-disable-next-line not-rely-on-time
145+
return ((block.timestamp - initialTimestamp) / periodSize);
146+
}
147+
148+
}

contracts/schemes/GenericSchemeMultiCall.sol

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2;
44
import "@daostack/infra/contracts/votingMachines/IntVoteInterface.sol";
55
import "@daostack/infra/contracts/votingMachines/ProposalExecuteInterface.sol";
66
import "../votingMachines/VotingMachineCallbacks.sol";
7+
import "./SchemeConstraints.sol";
78

89

910
/**
@@ -24,12 +25,10 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf
2425
}
2526

2627
mapping(bytes32=>MultiCallProposal) public proposals;
27-
2828
IntVoteInterface public votingMachine;
2929
bytes32 public voteParams;
30-
mapping(address=>bool) internal contractWhitelist;
31-
address[] public whitelistedContracts;
3230
Avatar public avatar;
31+
SchemeConstraints public schemeConstraints;
3332

3433
event NewMultiCallProposal(
3534
address indexed _avatar,
@@ -61,37 +60,26 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf
6160

6261
event ProposalDeleted(address indexed _avatar, bytes32 indexed _proposalId);
6362

64-
/**
65-
* @dev initialize
63+
/* @dev initialize
6664
* @param _avatar the avatar to mint reputation from
6765
* @param _votingMachine the voting machines address to
6866
* @param _voteParams voting machine parameters.
69-
* @param _contractWhitelist the contracts the scheme is allowed to interact with
70-
*
67+
* @param _schemeConstraints the schemeConstraints contracts.
7168
*/
7269
function initialize(
7370
Avatar _avatar,
7471
IntVoteInterface _votingMachine,
7572
bytes32 _voteParams,
76-
address[] calldata _contractWhitelist
73+
SchemeConstraints _schemeConstraints
7774
)
7875
external
7976
{
8077
require(avatar == Avatar(0), "can be called only one time");
8178
require(_avatar != Avatar(0), "avatar cannot be zero");
82-
require(_contractWhitelist.length > 0, "contractWhitelist cannot be empty");
8379
avatar = _avatar;
8480
votingMachine = _votingMachine;
8581
voteParams = _voteParams;
86-
/* Whitelist controller by default*/
87-
Controller controller = Controller(_avatar.owner());
88-
whitelistedContracts.push(address(controller));
89-
contractWhitelist[address(controller)] = true;
90-
91-
for (uint i = 0; i < _contractWhitelist.length; i++) {
92-
contractWhitelist[_contractWhitelist[i]] = true;
93-
whitelistedContracts.push(_contractWhitelist[i]);
94-
}
82+
schemeConstraints = _schemeConstraints;
9583
}
9684

9785
/**
@@ -127,28 +115,23 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf
127115
MultiCallProposal storage proposal = proposals[_proposalId];
128116
require(proposal.exist, "must be a live proposal");
129117
require(proposal.passed, "proposal must passed by voting machine");
118+
if (schemeConstraints != SchemeConstraints(0)) {
119+
require(
120+
schemeConstraints.isAllowedToCall(
121+
proposal.contractsToCall,
122+
proposal.callsData,
123+
proposal.values,
124+
avatar),
125+
"call is not allowed");
126+
}
130127
proposal.exist = false;
131128
bytes memory genericCallReturnValue;
132129
bool success;
133-
Controller controller = Controller(whitelistedContracts[0]);
134-
130+
Controller controller = Controller(avatar.owner());
135131
for (uint i = 0; i < proposal.contractsToCall.length; i++) {
136132
bytes memory callData = proposal.callsData[i];
137-
if (proposal.contractsToCall[i] == address(controller)) {
138-
(IERC20 extToken,
139-
address spender,
140-
uint256 valueToSpend
141-
) =
142-
abi.decode(
143-
callData,
144-
(IERC20, address, uint256)
145-
);
146-
success = controller.externalTokenApproval(extToken, spender, valueToSpend, avatar);
147-
} else {
148-
(success, genericCallReturnValue) =
149-
controller.genericCall(proposal.contractsToCall[i], callData, avatar, proposal.values[i]);
150-
}
151-
133+
(success, genericCallReturnValue) =
134+
controller.genericCall(proposal.contractsToCall[i], callData, avatar, proposal.values[i]);
152135
/* Whole transaction will be reverted if at least one call fails*/
153136
require(success, "Proposal call failed");
154137
emit ProposalCallExecuted(
@@ -190,19 +173,14 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf
190173
(_contractsToCall.length == _callsData.length) && (_contractsToCall.length == _values.length),
191174
"Wrong length of _contractsToCall, _callsDataLens or _values arrays"
192175
);
193-
for (uint i = 0; i < _contractsToCall.length; i++) {
176+
if (schemeConstraints != SchemeConstraints(0)) {
194177
require(
195-
contractWhitelist[_contractsToCall[i]], "contractToCall is not whitelisted"
196-
);
197-
if (_contractsToCall[i] == whitelistedContracts[0]) {
198-
199-
(, address spender,) =
200-
abi.decode(
201-
_callsData[i],
202-
(IERC20, address, uint256)
203-
);
204-
require(contractWhitelist[spender], "spender contract not whitelisted");
205-
}
178+
schemeConstraints.isAllowedToPropose(
179+
_contractsToCall,
180+
_callsData,
181+
_values,
182+
avatar),
183+
"propose is not allowed");
206184
}
207185

208186
proposalId = votingMachine.propose(2, voteParams, msg.sender, address(avatar));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
pragma solidity 0.5.17;
2+
pragma experimental ABIEncoderV2;
3+
import "../controller/Avatar.sol";
4+
5+
6+
contract SchemeConstraints {
7+
8+
address[] public contractsWhiteList;
9+
10+
/*
11+
* @dev isAllowedToCall should be called upon a proposal execution.
12+
* @param _contractsToCall the contracts to be called
13+
* @param _callsData - The abi encode data for the calls
14+
* @param _values value(ETH) to transfer with the calls
15+
* @param _avatar avatar
16+
* @return bool value true-allowed false not allowed
17+
*/
18+
function isAllowedToCall(
19+
address[] calldata _contractsToCall,
20+
bytes[] calldata _callsData,
21+
uint256[] calldata _values,
22+
Avatar _avatar)
23+
external returns(bool);
24+
25+
/*
26+
* @dev isAllowedToPropose should be called upon a proposal submition.
27+
* @param _contractsToCall the contracts to be called
28+
* @param _callsData - The abi encode data for the calls
29+
* @param _values value(ETH) to transfer with the calls
30+
* @param _avatar avatar
31+
* @return bool value true-allowed false not allowed
32+
*/
33+
function isAllowedToPropose(
34+
address[] calldata _contractsToCall,
35+
bytes[] calldata _callsData,
36+
uint256[] calldata _values,
37+
Avatar _avatar)
38+
external returns(bool);
39+
40+
function getContractsWhiteList() external view returns(address[] memory) {
41+
return contractsWhiteList;
42+
}
43+
44+
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@daostack/arc",
3-
"version": "0.0.1-rc.45",
3+
"version": "0.0.1-rc.46",
44
"description": "A platform for building DAOs",
55
"files": [
66
"contracts/",

0 commit comments

Comments
 (0)