Quiet Ocean Boar
Medium
The protocol describes some invariants that should not be broken, these invariants can be observed below:
One address may be associated with maximum one profile. Profile creation requires at least one active invitation. One may not write to Ethos contracts until a (non-mock) profile is created.
to be specific the invariant One may not write to Ethos contracts until a (non-mock) profile is created.
is broken because it is possible for a user to call archiveProfile
, restoreProfile
, and registerAddress
with a mock account, and thus breaking the invariant since the functions archiveProfile
, restoreProfile
, and registerAddress
writes to an ethos contract.
in EthosReview.sol
ln 174
function addReview(
Score score,
address subject,
address paymentToken,
string calldata comment,
string calldata metadata,
AttestationDetails calldata attestationDetails
) external payable whenNotPaused {
the function addReview
allows a profile to leave a review for a subject
. If the subject does not hold an ethos profile, a mock will be made for them. The problem is that by inputting specific values, a user can make a mock account and then call archiveProfile
, restoreProfile
, and registerAddress
to write to the the ethosProfile contract and thus breaking the invariant.
Let me breakdown how this is done...
first a user must call addReview
with subject that != 0 and a subject
address that does not currently own a profile on the EthosProfile contract. The subject
Address must be owned by the user to pull the remaining part of the exploit of aswell.
now lets go line by line to see the outcome...
_validateReviewDetails(subject, attestationDetails);
with the following configuration this check will pass next...
if (subject != address(0)) {
mockId = ethosProfile.profileIdByAddress(subject);
mockId = _addReview(mockId, subject, false, 0x0, ethosProfile);
This if statement will be triggered which is what we need to pull of the exploit, the call ethosProfile.profileIdByAddress(subject)
will return 0 because as i stated before subject
address is currently not associated with a profileId and thus the default value will be returned which is 0, next mockID
is set to 0 and we call _addReview
with the mockID
= 0. Lets observe the consequence of this
function _addReview(
uint256 mockId,
address subject,
bool isAttestation,
bytes32 attestationHash,
IEthosProfile ethosProfile
) internal returns (uint256 subjectProfileId) {
// if profileId does not exist for subject, create and record a "mock"
if (mockId == 0) {
subjectProfileId = ethosProfile.incrementProfileCount(
isAttestation,
subject,
attestationHash
);
the first if statement is triggered and thus we will call incrementProfileCount
let us see what happens next
function incrementProfileCount(
bool isAttestation,
address subject,
bytes32 attestation
) external whenNotPaused returns (uint256 profileId) {
if (
msg.sender != contractAddressManager.getContractAddressForName(ETHOS_REVIEW) &&
msg.sender != contractAddressManager.getContractAddressForName(ETHOS_ATTESTATION)
) {
revert InvalidSender();
}
profileId = profileCount;
if (isAttestation) {
profileIdByAttestation[attestation] = profileId;
} else {
profileIdByAddress[subject] = profileId;
}
profileCount++;
emit MockProfileCreated(profileId);
}
We hit the else statement and we assign the subjectAddress
(which is the users other wallet) to the next profileID
Now that the users wallet has been assigned a profileID the user has officially associated his address with a mock account. And since the mock account has a profileId, it no longer returns variable mock
as true in the following functions:
function profileStatusById(
uint256 profileId
) public view returns (bool verified, bool archived, bool mock) {
Profile storage profile = profiles[profileId];
verified = profile.profileId > 0;
archived = verified && profile.archived;
mock = profileId > 0 && !verified && profileId < profileCount;
}
/**
* @dev Returns the status of a profile by its associated address.
* @param addressStr The address to check.
* @return verified Whether the profile is verified.
* @return archived Whether the profile is archived.
* @return mock Whether the profile is a mock profile.
* @return profileId The ID of the profile associated with the address.
*/
function profileStatusByAddress(
address addressStr
) public view returns (bool verified, bool archived, bool mock, uint256 profileId) {
profileId = profileIdByAddress[addressStr];
(verified, archived, mock) = profileStatusById(profileId);
}
these 2 checks are important to ensure a mock cannot write to the ethos contracts and i have shown how a user can take control of a mock account and with the same mock account bypass these checks. Now that he can pass these checks the mock account can do almost everything a regular account can do. Including calling archiveProfile
, restoreProfile
, and registerAddress
. (note: can also call function addReview
, addReply
, and voteFor
found this out after writing report)
Therefore the invariant One may not write to Ethos contracts until a (non-mock) profile is created.
is broken.
and according to sherlock docs;
The protocol team can use the README (and only the README) to define language that indicates the codebase's restrictions and/or expected functionality. Additionally, the protocol team can use only the following question to define the protocol's invariants/properties:
What properties/invariants do you want to hold even if breaking them has a low/unknown impact?
Issues that break the invariants from the above question, irrespective of whether the impact is low/unknown, will be assigned Medium severity. High severity will be applied only if the issue falls into the High severity category in the judging guidelines.
Example: The README states "Admin can only call XYZ function once" but the code allows the Admin to call XYZ function twice; this is a valid Medium
This will qualify this issue as a valid medium.
- address must not have a profile on ethosprofile
none
- the address does not have an ethos profile associated to it
- the user must call addReview with the address
- the address will be assigned to a mock
- the address can call
archiveProfile
,restoreProfile
, andregisterAddress
- a mock has been used to write to an ethos contract
- the invariant
One may not write to Ethos contracts until a (non-mock) profile is created.
is broken
breaking of a protocol invariant, allows a mock to write to an ethos contract
No response
Since the mock the user is controlling does not set the following values
profiles[profileId].profileId = profileId;
profiles[profileId].createdAt = block.timestamp;
profiles[profileId].inviteInfo.available = defaultNumberOfInvites;
profiles[profileId].addresses.push(user);
We can prevent a mock account controlled by a user to interact with the protocol by using a check createdAt>0 in the functions archiveProfile
, restoreProfile
, and registerAddress
This fix is not perfect so id recommend thinking about a fix that is more in depth and ensures that the invariant holds.