Platform: Code4rena
Start Date: 05/09/2023
Pot Size: $50,000 USDC
Total HM: 2
Participants: 16
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 284
League: ETH
Rank: 4/16
Findings: 1
Award: $927.61
🌟 Selected for report: 1
🚀 Solo Findings: 0
927.6135 USDC - $927.61
I first explored the scope of audit. I discovered that the project can be divided into 2 independent parts: Delegate Registry
and Delegate Marketplace
. I carried out all subsequent stages separately for each of this parts, and then analyzed the correctness of their interaction.
Delegate Registry
:
Test coverage is 100% for most audit files. In this regard, I decided to concentrate on finding logical errors, since simple errors (errors due to typos, incorrect statements) should be excluded by tests.
File | % Lines | % Statements | % Branches | % Funcs |
---|---|---|---|---|
src/DelegateRegistry.sol | 100.00% (175/175) | 100.00% (219/219) | 98.78% (81/82) | 100.00% (33/33) |
src/libraries/RegistryHashes.sol | 100.00% (12/12) | 100.00% (12/12) | 100.00% (0/0) | 100.00% (12/12) |
src/libraries/RegistryOps.sol | 66.67% (2/3) | 66.67% (2/3) | 100.00% (0/0) | 66.67% (2/3) |
src/libraries/RegistryStorage.sol | 100.00% (6/6) | 100.00% (6/6) | 100.00% (0/0) | 100.00% (3/3) |
Delegate Marketplace
:
File | % Lines | % Statements | % Branches | % Funcs |
---|---|---|---|---|
src/CreateOfferer.sol | 90.70% (39/43) | 92.00% (46/50) | 77.27% (17/22) | 100.00% (8/8) |
src/DelegateToken.sol | 88.55% (147/166) | 90.28% (195/216) | 80.43% (37/46) | 89.66% (26/29) |
src/PrincipalToken.sol | 100.00% (14/14) | 100.00% (17/17) | 100.00% (4/4) | 100.00% (5/5) |
src/libraries/CreateOffererLib.sol | 95.24% (40/42) | 95.38% (62/65) | 69.23% (18/26) | 100.00% (9/9) |
src/libraries/DelegateTokenLib.sol | 88.89% (8/9) | 90.48% (19/21) | 75.00% (6/8) | 100.00% (5/5) |
src/libraries/DelegateTokenRegistryHelpers.sol | 100.00% (57/57) | 100.00% (87/87) | 100.00% (26/26) | 100.00% (21/21) |
src/libraries/DelegateTokenStorageHelpers.sol | 91.67% (44/48) | 92.11% (70/76) | 80.77% (21/26) | 100.00% (21/21) |
src/libraries/DelegateTokenTransferHelpers.sol | 88.24% (30/34) | 87.80% (36/41) | 80.77% (21/26) | 100.00% (9/9) |
I studied the Delegate Registry
code starting with the libraries, and also starting from the lowest level functions, moving to the top level functions. Having built a general understanding of what each of the functions does, I formed an idea of how the Registry works and built general diagrams.
There are also 5 functions to check the correctness of the delegation
<details> <summary>Details on each function and hashing schemes</summary>RegistryOps
library:
max
: use optimized assembly logic to calculate max of 2 numbersand
: use 2x iszero
to clean arguments before and
or
: use 2x iszero
to clean arguments before or
RegistryStorage
library:
packAddresses
: - store from
, to
and contract
addresses in 2 storage slotsunpackAddresses
: - reverse to packAddresses
operationunpackAddress
: - helper to unpack to
or from
. Should not to be used for contract
unwrappingRegistryHashes
library:
decodeType
: - decode hash type from last byte into enum
, (potentially may overflow enum
)
location
: - calculate storage key from hash
allHash
: - calculate hash for all
type
allLocation
: - calculate location for all
type hash
contractHash
: - similar to allHash
contractLocation
: - similar to allLocation
erc721Hash
: - similar to allHash
erc721Location
: - similar to allLocation
erc20Hash
: - similar to allHash
erc20Location
: - similar to allLocation
erc1155Hash
: - similar to allHash
erc1155Location
: - similar to allLocation
DelegateRegistry
contract:
delegations
outgoingDelegationHashes
incomingDelegationHashes
sweep
: - transfer all contract balance to hardcoded address (Currently 0x0)
readSlot
: - perform sload
readSlots
: - perform sload
s in loop
_pushDelegationHashes
: - push delegation hash to the incoming and outgoing hashes mappings
_writeDelegation
x2 : - perform sstore
for data
at position
in location
_updateFrom
: - change from
value in first slot, while keeping first 8 bytes of contract
intact
_loadDelegationBytes32
: - perform sload
at position
in location
_loadDelegationUint
: - similar to _loadDelegationBytes32
multicall
: - payable multicall
supportsInterface
: -
_writeDelegationAddresses
: - sstore
packed delegation at 0 and 1 slot in location
_loadFrom
: - sload
from
address from location
_loadDelegationAddresses
: - reverse to _writeDelegationAddresses
_invalidFrom
: - check if address is DELEGATION_EMPTY
or DELEGATION_REVOKED
flags(addresses)
_validateFrom
: - match passed from
to value in location
checkDelegateForAll
: - validate that from
delegated to
the entire wallet
checkDelegateForContract
: - the same as checkDelegateForContract
or delegated for specific contract
checkDelegateForERC721
: - the same as checkDelegateForContract
or delegated for specific tokenId
in specific contract
checkDelegateForERC20
: - return amount delegated from
to to
checkDelegateForERC1155
: - similar to checkDelegateForERC20
_getValidDelegationHashesFromHashes
: - remove invalid from
s from hashes array
getIncomingDelegationHashes
: - return only valid hashes from incomingDelegationHashes
getOutgoingDelegationHashes
: - the same as getIncomingDelegationHashes
, but with outgoingDelegationHashes
_getValidDelegationsFromHashes
: - read storage for every valid hash in memory Delegation
struct
getIncomingDelegations
: - return Delegation
struct for only valid hashes from incomingDelegationHashes
getOutgoingDelegations
: - the same as getIncomingDelegations
, but with outgoingDelegationHashes
getDelegationsFromHashes
: - the same as _getValidDelegationsFromHashes
but for invalid delegation return empty struct
delegateAll
: - msg.sender
delegate the whole wallet to from
delegateContract
: - similar to delegateAll
, but for specific contract
delegateERC721
: - similar to delegateContract
, but for specific tokenId
delegateERC20
: - similar to delegateContract
, but for ERC20
token amount
+ allow to change amount
if already delegated
delegateERC1155
: - similar to delegateERC20
, but for ERC1155
(specific tokenId
)
Delegate Marketplace
:
CreateOfferer
is a separate part of the marketplace that guarantees interaction with the seaport.
DelegateToken
and PrincipalToken
tokens. Transfer one of token types to contract. Delegate to `delegateHolder``. mint principal token.DelegateToken
. Called by PrincipalToken
owner.DelegateToken
to the PrincipalToken
holder early. Called by DelegateToken
holder or after the DelegateToken
has expired, anyone can call this method. this does not release the spot asset from escrow, it merely cancels out the DelegateToken
.PrincipalToken
and claim the spot asset from escrow. Called by the PrincipalToken
owner. PrincipalToken
owner can authorize others to call this on their behalf, and if PrincipalToken
owner also owns the DelegateToken
then they can skip calling rescind and go straight to withdrawDelegateTokenStorageHelpers
library:
writeApproved
: - store approved
to *PACKED_INFO_POSITION*
while keeping expiry
intactwriteExpiry
: - store expiry
to *PACKED_INFO_POSITION*
while keeping approved
intactwriteRegistryHash
: - store registryHash
to REGISTRY_HASH_POSITION
writeUnderlyingAmount
: - store underlyingAmount
to UNDERLYING_AMOUNT_POSITION
incrementBalance
: - increment balance
for delegateTokenHolder
decrementBalance
: - decrement balance
for delegateTokenHolder
principalIsCaller
: - revert if msg.sender
is not principalToken
revertAlreadyExisted
: - revert if registryHash
is not zerorevertNotOperator
: - revert if not operator
or “owner”readApproved
: - shift PACKED_INFO_POSITION
to read approved
readExpiry
: - read expiry
from PACKED_INFO_POSITION
readRegistryHash
: - read registryHash
from REGISTRY_HASH_POSITION
readUnderlyingAmount
: - read underlyingAmount
from UNDERLYING_AMOUNT_POSITION
revertNotMinted
: - revert if registryHash
is not set or used (ID_AVAILABLE
, ID_USED
)checkBurnAuthorized
: - revert if caller is not principalToken
or delegate not authorized burncheckMintAuthorized
: - similar to checkBurnAuthorized
but with mintrevertNotApprovedOrOperator
: - revert if caller is not “owner” or operator
or approved
in tokenrevertInvalidWithdrawalConditions
: - similar to revertNotApprovedOrOperator
+ check expiryburnPrincipal
: - call burn
on PrincipalToken
with custom reentrancy guardmintPrincipal
: - call mint
on PrincipalToken
with custom reentrancy guardDelegateTokenRegistryHelpers
library:
loadTokenHolder
: - read to
from delegateRegistry
at location
from registryHash
. Not revert on revoked!!!loadContract
: - read contract
from delegateRegistry
at location
from registryHash
loadTokenHolderAndContract
: - read to
and contract
from delegateRegistry
at location
from registryHash
loadFrom
: - similar with from
loadAmount
: - similar with amount
loadRights
: - similar with rights
loadTokenId
: - similar with tokenId
calculateDecreasedAmount
: - return amount
- decreaseAmount
. No underflow check!!!calculateIncreasedAmount
: - similar to calculateDecreasedAmount
,but increasedtransferERC721
: - revoke delegation to from
and delegate to
while validating both hashesrevokeERC721
: - revoke delegation and validate hashdelegateERC721
: - delegate and validate hashrevertERC721FlashUnavailable
: - revert if contract
does not have rights
for flashloan
or tokenId
itselfrevertERC20FlashAmountUnavailable
: - revert if delegation does not have enough amount
with “”
and flashloan
rights
revertERC1155FlashAmountUnavailable
: - similar to revertERC20FlashAmountUnavailable
transferERC20
: - decrease amount
from old delegation and increase for newtransferERC1155
: - similar to transferERC20
incrementERC20
: - increase amount in delegationincrementERC1155
: - the same with ERC1155
decrementERC20
: - similar to incrementERC20
, but decreasedecrementERC1155
: - similar to incrementERC1155
, but decreaseDelegateTokenTransferHelpers
library:
ERC1155
callbackscheckERC1155BeforePull
: - custom reentrancy guard + revert if amount == 0checkERC1155Pulled
:- bottom part of custom reentrancy guard + require contract to be operatorrevertInvalidERC1155PullCheck
: - revert on checkERC1155Pulled
conditionpullERC1155AfterCheck
: - transfer ERC1155
from msg.sender
to contract revert if ERC1155_PULLED
checkERC20BeforePull
: - check that it is ERC20
check that amount
≠0, check that there is enough allowance
pullERC20AfterCheck
: - transfer ERC20
from msg.sender
to contractcheckERC721BeforePull
: - check that it is ERC721
, check that owner is msg.sender
pullERC721AfterCheck
: - transfer ERC721
from msg.sender
to contractcheckAndPullByType
: - transfer one of token types from msg.sender
to contractDelegateTokenHelpers
library:
revertOnCallingInvalidFlashloan
: - revert if selector does not matchrevertOnInvalidERC721ReceiverCallback
: - the samerevertOnInvalidERC721ReceiverCallback
: - the samerevertOldExpiry
: - revert if expiry
expireddelegateIdNoRevert
: - hash caller
and salt
DelegateToken
contract:
supportsInterface
: - supported interfacesonERC1155BatchReceived
: - revertonERC721Received
: - revert if contract is not operator
, else return selectoronERC1155Received
: - revert on custom reentrancy check fail else return selectorbalanceOf
: - get balance of delegateTokenHolder
if not address(0)
ownerOf
: - return to
from registry for specific delegateTokenId
getApproved
: - return approved
address revert if not mintedisApprovedForAll
: - return if accountOperator
approve
: - store approved spender
, revert if not minted or not operatorsetApprovalForAll
: - set accountOperator
name
: - constantsymbol
: - constanttransferFrom
: - transfer delegateTokenId
with underlying tokenisApprovedOrOwner
: - check if it is “owner"
or operator
or approved
getDelegateId
: - get delegateTokenId
revert if not availableburnAuthorizedCallback
: - revert if caller is not principalToken
or delegate not authorized burnmintAuthorizedCallback
: - similarcreate
: - transfer one of token types to contract. delegate to delegateHolder
. mint principal tokensafeTransferFrom
: - call transferFrom
and check selector callbackgetDelegateInfo
: - build and get DelegateInfo
from delegateTokenId
extend
: - allow principal or operator to increase expiry
if old not expiredrescind
: - allow delegate( or anyone after expiry) transfer delegateTokenId
to principaltokenURI
: - call MarketMetadata
for delegateTokenURI
baseURI
: - call MarketMetadata
for delegateTokenBaseURI
contractURI
: - call MarketMetadata
for delegateTokenContractURI
royaltyInfo
: - similarwithdraw
: - withdraw delegation, burn principal, transfer underlaying back to msg.sender
flashloan
: - flash-loan operation for all token typesPrincipalToken
contract:
isApprovedOrOwner
: - Call ERC721
_isApprovedOrOwner
_checkDelegateTokenCaller
: - check caller is delegateToken
tokenURI
: - Call MarketMetadata
for principalTokenURI
mint
: - mint. Called by delegateToken
when authorizedburn
: - burn. Called by delegateToken
when authorizedCreateOffererModifiers
library:
seaport
address and StageonlySeaport
: - caller is seaport
checkStage
: - reentrancy check + stage changeCreateOffererHelpers
library:
processNonce
: - check nonce and increment if correctupdateTransientState
: - fulfill TransientState
structcreateAndValidateDelegateTokenId
: - Call create
on IDelegateToken
. And check correct delegateId
calculateExpiry
: - return absolute expiry
for both typesprocessSpentItems
: - build offer
and consideration
from minimumReceived
and maximumSpent
calculateOrderHash
: - hash order
with with tokenType
calculateOrderHashAndId
: - get delegateTokenId
from calculateOrderHash
verifyCreate
: - match hash to contextvalidateCreateOrderHash
: - match provided hash to actualCreateOfferer
contract:
The contract consists of 2 parts, one part is a storage of delegation hashes, and the other part is ERC721 compatible tokens that reflect the ownership of the delegation.
Delegate Registry
: uses hashing to compactly store the delegation. And also hash functions for calculating a unique location in storage. It also contains functions that check the hash based on the location and from
address.
Delegate Token
: Deposits all assets, in return issues an ERC721 token, which confirms the ownership of the delegation, for a certain period of time.
PrincipalToken
: Depends on Delegate Token
, cannot be called on its own. It is an ERC721 token that confirms the right to claim deposited assets after expiration.
CreateOfferer
: Integration with seaport as specified in documentation. When selling, the asset turns into a Delegate Token
and is assigned to the buyer, the seller receives a PrincipalToken
.
Delegate Token
in its work relies entirely on Delegate Registry
, which must reliably guarantee the authenticity and confirmation of the delegation.
In general, the quality of the code base is quite high. The huge number of comments in NatSpec makes it very easy to determine what a particular function is intended for.
The downside is the use of assembler for gas optimization, which is not comparable to the damage it causes to code readability.
There is no risk of centralization since all rights are divided between the Delegate Token
and the PrincipalToken
. The only exception is CreateOfferer
, which relies on the seaport
address, which is immutable, but it is possible that the contract address will change in the future, it would be useful to add a function that allows you to change the address if necessary
The contract is used to delegate all types of tokens (ERC20
, ERC721
, ERC1155
), but does not take into account that some tokens do not follow the standards.
Contracts are programmed for version ^0.8.21, by default the compiler will use version 0.8.21, which is very recent and may contain undetected vulnerabilities, as well as compatibility problems with different L2 chains.
I learned about CreateOfferer
seaport integration, all other concepts were well known to me.
33 hours
#0 - c4-judge
2023-09-24T16:09:43Z
GalloDaSballo marked the issue as selected for report
#1 - GalloDaSballo
2023-09-24T16:12:10Z
Imo proper way to discuss coverage + interesting charts for packing and logic on delegation
#2 - c4-judge
2023-09-24T16:12:15Z
GalloDaSballo marked the issue as grade-a