-
Notifications
You must be signed in to change notification settings - Fork 11
/
CalldataValidation.sol
215 lines (208 loc) · 7.93 KB
/
CalldataValidation.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
import "../libraries/Strings.sol";
import "../libraries/AbiDecoder.sol";
import "../../interfaces/IAllowlist.sol";
/**
* @title Validate raw calldata against an allowlist
* @author yearn.finance
*/
/*******************************************************
* Main Contract Logic
*******************************************************/
library CalldataValidation {
/**
* @notice Calculate a method signature given a condition
* @param condition The condition from which to generate the signature
* @return signature The method signature in string format (ie. "approve(address,uint256)")
*/
function methodSignatureByCondition(IAllowlist.Condition memory condition)
public
pure
returns (string memory signature)
{
bytes memory signatureBytes = abi.encodePacked(condition.methodName, "(");
for (uint256 paramIdx; paramIdx < condition.paramTypes.length; paramIdx++) {
signatureBytes = abi.encodePacked(
signatureBytes,
condition.paramTypes[paramIdx]
);
if (paramIdx + 1 < condition.paramTypes.length) {
signatureBytes = abi.encodePacked(signatureBytes, ",");
}
}
signatureBytes = abi.encodePacked(signatureBytes, ")");
signature = string(signatureBytes);
}
/**
* @notice Check target validity
* @param implementationAddress The address the validation method will be executed against
* @param targetAddress The target address to validate
* @param requirementValidationMethod The method to execute
* @return targetValid Returns true if the target is valid and false otherwise
* @dev If "requirementValidationMethod" is "isValidVaultToken" and target address is usdc
* the validation check will look like this: usdc.isValidVaultToken(targetAddress),
* where the result of the validation method is expected to return a bool
*/
function checkTarget(
address implementationAddress,
address targetAddress,
string memory requirementValidationMethod
) public view returns (bool targetValid) {
string memory methodSignature = string(
abi.encodePacked(requirementValidationMethod, "(address)")
);
(, bytes memory data) = address(implementationAddress).staticcall(
abi.encodeWithSignature(methodSignature, targetAddress)
);
targetValid = abi.decode(data, (bool));
}
/**
* @notice Check method selector validity
* @param data Raw input calldata (we will extract the 4-byte selector
* from the beginning of the calldata)
* @param condition The condition struct to check (we generate the complete
* method selector using condition.methodName and condition.paramTypes)
* @return methodSelectorValid Returns true if the method selector is valid and false otherwise
*/
function checkMethodSelector(
bytes calldata data,
IAllowlist.Condition memory condition
) public pure returns (bool methodSelectorValid) {
string memory methodSignature = methodSignatureByCondition(condition);
bytes4 methodSelectorBySignature = bytes4(
keccak256(bytes(methodSignature))
);
bytes4 methodSelectorByCalldata = bytes4(data[0:4]);
methodSelectorValid = methodSelectorBySignature == methodSelectorByCalldata;
}
/**
* @notice Check an individual method param's validity
* @param implementationAddress The address the validation method will be executed against
* @param requirement The specific requirement (of type "param") to check (ie. ["param", "isVault", "0"])
* @dev A condition may have multiple requirements, all of which must be true
* @dev The middle element of a requirement is the requirement validation method
* @dev The last element of a requirement is the parameter index to validate against
* @param condition The entire condition struct to check the param against
* @param data Raw input calldata for the original method call
* @return Returns true if the param is valid, false if not
*/
function checkParam(
address implementationAddress,
string[] memory requirement,
IAllowlist.Condition memory condition,
bytes calldata data
) public view returns (bool) {
uint256 paramIdx = Strings.atoi(requirement[2], 10);
string memory paramType = condition.paramTypes[paramIdx];
bytes memory paramCalldata = AbiDecoder.getParamFromCalldata(
data,
paramType,
paramIdx
);
string memory methodSignature = string(
abi.encodePacked(requirement[1], "(", paramType, ")")
);
bytes memory encodedCalldata = abi.encodePacked(
bytes4(keccak256(bytes(methodSignature))),
paramCalldata
);
bool success;
bytes memory resultData;
(success, resultData) = address(implementationAddress).staticcall(
encodedCalldata
);
if (success) {
return abi.decode(resultData, (bool));
}
return false;
}
/**
* @notice Test a target address and calldata against a specific condition and implementation
* @param condition The condition to test
* @param targetAddress Target address of the original method call
* @param data Calldata of the original methodcall
* @return Returns true if the condition passes and false if not
* @dev The condition check is comprised of 3 parts:
- Method selector check (to make sure the calldata method selector matches the condition method selector)
- Target check (to make sure the target is valid)
- Param check (to make sure the specified param is valid)
*/
function testCondition(
address allowlistAddress,
IAllowlist.Condition memory condition,
address targetAddress,
bytes calldata data
) public view returns (bool) {
string[][] memory requirements = condition.requirements;
address implementationAddress = IAllowlist(allowlistAddress)
.implementationById(condition.implementationId);
for (
uint256 requirementIdx;
requirementIdx < requirements.length;
requirementIdx++
) {
string[] memory requirement = requirements[requirementIdx];
string memory requirementType = requirement[0];
string memory requirementValidationMethod = requirement[1];
if (!checkMethodSelector(data, condition)) {
return false;
}
if (Strings.stringsEqual(requirementType, "target")) {
bool targetValid = checkTarget(
implementationAddress,
targetAddress,
requirementValidationMethod
);
if (!targetValid) {
return false;
}
} else if (Strings.stringsEqual(requirementType, "param")) {
bool paramValid = checkParam(
implementationAddress,
requirement,
condition,
data
);
if (!paramValid) {
return false;
}
}
}
return true;
}
/**
* @notice Test target address and calldata against all stored protocol conditions
* @dev This is done to determine whether or not the target address and calldata are valid and whitelisted
* @dev This is the primary method that should be called by integrators
* @param allowlistAddress The address of the allowlist to check calldata against
* @param targetAddress The target address of the call
* @param data The raw calldata to test
* @return Returns true if the calldata/target test is successful and false if not
*/
function validateCalldataByAllowlist(
address allowlistAddress,
address targetAddress,
bytes calldata data
) public view returns (bool) {
IAllowlist.Condition[] memory _conditions = IAllowlist(allowlistAddress)
.conditionsList();
for (
uint256 conditionIdx;
conditionIdx < _conditions.length;
conditionIdx++
) {
IAllowlist.Condition memory condition = _conditions[conditionIdx];
bool conditionPassed = testCondition(
allowlistAddress,
condition,
targetAddress,
data
);
if (conditionPassed) {
return true;
}
}
return false;
}
}