Platform: Code4rena
Start Date: 15/07/2021
Pot Size: $80,000 USDC
Total HM: 28
Participants: 18
Period: 7 days
Judge: ghoulsol
Total Solo HM: 18
Id: 20
League: ETH
Rank: 6/18
Findings: 8
Award: $4,356.20
🌟 Selected for report: 8
🚀 Solo Findings: 2
124.9539 USDC - $124.95
gpersoon
A call to transferFrom or transfer is frequently done without checking the results. For certain ERC20 tokens, if insufficient tokens are present, no revert occurs but a result of "false" is returned. So its important to check this. If you don't you could mint tokens without have received sufficient tokens to do so. So you could loose funds.
Its also a best practice to check this. See below for example where the result isn't checked.
Note, in some occasions the result is checked (see below for examples).
Highest risk: .\Dao.sol: iBEP20(_token).transferFrom(msg.sender, address(this), _amount); // Transfer user's assets to Dao contract .\Pool.sol: iBEP20(TOKEN).transfer(member, outputToken); // Transfer the TOKENs to user .\Pool.sol: iBEP20(token).transfer(member, outputAmount); // Transfer the swap output to the selected user .\poolFactory.sol: iBEP20(_token).transferFrom(msg.sender, _pool, _amount); .\Router.sol: iBEP20(_fromToken).transfer(fromPool, iBEP20(_fromToken).balanceOf(address(this))); // Transfer TOKENs from ROUTER to fromPool .\Router.sol: iBEP20(_token).transfer(_pool, iBEP20(_token).balanceOf(address(this))); // Transfer TOKEN to pool .\Router.sol: iBEP20(_token).transferFrom(msg.sender, _pool, _amount); // Transfer TOKEN to pool .\Router.sol: iBEP20(_token).transfer(_recipient, _amount); // Transfer TOKEN to recipient .\Synth.sol: iBEP20(_token).transferFrom(msg.sender, address(this), _amount); // Transfer tokens in
less risky .\Router.sol: iBEP20(fromPool).transferFrom(_member, fromPool, unitsInput); // Transfer LPs from user to the pool .\BondVault.sol: iBEP20(_pool).transfer(member, _claimable); // Send claim amount to user .\Router.sol: iBEP20(_pool).transferFrom(_member, _pool, units); // Transfer LPs to the pool .\Router.sol: iBEP20(_pool).transferFrom(_member, _pool, units); // Transfer LPs to pool .\Router.sol: iBEP20(fromSynth).transferFrom(msg.sender, _poolIN, inputAmount); // Transfer synth from user to pool .\Pool.sol: iBEP20(synthIN).transfer(synthIN, _actualInputSynth); // Transfer SYNTH to relevant synth contract .\Router.sol: iBEP20(WBNB).transfer(_pool, _amount); // Transfer WBNB from ROUTER to pool .\Dao.sol: iBEP20(BASE).transfer(newDAO, baseBal); .\Pool.sol: iBEP20(BASE).transfer(member, outputBase); // Transfer the SPARTA to user .\Pool.sol: iBEP20(BASE).transfer(member, outputBase); // Transfer SPARTA to user .\Router.sol: iBEP20(BASE).transfer(toPool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA from ROUTER to toPool .\Router.sol: iBEP20(BASE).transfer(_pool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA to pool .\Router.sol: iBEP20(BASE).transfer(_pool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA from ROUTER to pool .\Router.sol: iBEP20(BASE).transferFrom(msg.sender, _pool, inputAmount); // Transfer SPARTA from ROUTER to pool
Sometimes the result is checked: .\Dao.sol: require(iBEP20(pool).transferFrom(msg.sender, address(_DAOVAULT), amount), "!funds"); // Send user's deposit to the DAOVault .\Dao.sol: require(iBEP20(BASE).transferFrom(msg.sender, address(_RESERVE), _amount), '!fee'); // User pays the new proposal fee .\DaoVault.sol: require(iBEP20(pool).transfer(member, _balance), "!transfer"); // Transfer user's balance to their wallet .\synthVault.sol: require(iBEP20(synth).transferFrom(msg.sender, address(this), amount)); // Must successfuly transfer in .\synthVault.sol: require(iBEP20(synth).transfer(msg.sender, redeemedAmount)); // Transfer from SynthVault to user
grep
Always check the result of transferFrom and transfer
#0 - verifyfirst
2021-07-21T02:05:59Z
The intention was to not allow non-standard tokens with non-boolean returns however in the interest of future proofing the protocol we agree with this issue
#1 - ghoul-sol
2021-08-08T20:44:52Z
There are a lot of reported issues in relation of non-standard ERC20 and transfer
return values. Some wardens report it all in one issue, some divided it into multiple issues. To keep playing field equal, I'll keep one issue per warden and make others invalid.
gpersoon
You can keep the DAO voting system hostage by repeatedly adding useless proposals. You would have to monitor the blockchain and as soon as a new proposal can be made, make a new useless proposal. (submit with a high fee and/or via a high priority channel like flashbots, taichi, bloxroute, archer)
A new proposal can only be submitted when mapPID_open[currentProposal] == false (see checkProposal() ) The contract can only reach this state via cancelProposal() or finaliseProposal()&completeProposal() Both cancelProposal() and finaliseProposal() force waiting for quite a while:
https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L406
function checkProposal() internal { require(mapPID_open[currentProposal] == false, '!open'); // There must not be an existing open proposal proposalCount += 1; // Increase proposal count currentProposal = proposalCount; // Set current proposal to the new count mapPID_open[currentProposal] = true; // Set new proposal as open status mapPID_startTime[currentProposal] = block.timestamp; // Set the start time of the proposal to now }
function cancelProposal() external { require(block.timestamp > (mapPID_startTime[currentProposal] + 1296000), "!days"); // Proposal must not be new mapPID_votes[currentProposal] = 0; // Clear all votes from the proposal mapPID_open[currentProposal] = false; // Set the proposal as not open (closed status) emit CancelProposal(msg.sender, currentProposal); }
// A finalising-stage proposal can be finalised after the cool off period function finaliseProposal() external { require((block.timestamp - mapPID_coolOffTime[currentProposal]) > coolOffPeriod, "!cooloff"); // Must be past cooloff period require(mapPID_finalising[currentProposal] == true, "!finalising"); // Must be in finalising stage if(!hasQuorum(currentProposal)){ mapPID_finalising[currentProposal] = false; // If proposal has lost quorum consensus; kick it out of the finalising stage } else { bytes memory _type = bytes(mapPID_type[currentProposal]); // Get the proposal type if(isEqual(_type, 'DAO')){ moveDao(currentProposal); } else if (isEqual(_type, 'ROUTER')) { moveRouter(currentProposal); } else if (isEqual(_type, 'UTILS')){ moveUtils(currentProposal); } else if (isEqual(_type, 'RESERVE')){ moveReserve(currentProposal); } else if (isEqual(_type, 'FLIP_EMISSIONS')){ flipEmissions(currentProposal); } else if (isEqual(_type, 'COOL_OFF')){ changeCooloff(currentProposal); } else if (isEqual(_type, 'ERAS_TO_EARN')){ changeEras(currentProposal); } else if (isEqual(_type, 'GRANT')){ grantFunds(currentProposal); } else if (isEqual(_type, 'GET_SPARTA')){ _increaseSpartaAllocation(currentProposal); } else if (isEqual(_type, 'LIST_BOND')){ _listBondingAsset(currentProposal); } else if (isEqual(_type, 'DELIST_BOND')){ _delistBondingAsset(currentProposal); } else if (isEqual(_type, 'ADD_CURATED_POOL')){ _addCuratedPool(currentProposal); } else if (isEqual(_type, 'REMOVE_CURATED_POOL')){ _removeCuratedPool(currentProposal); } } }
function completeProposal(uint _proposalID) internal { string memory _typeStr = mapPID_type[_proposalID]; // Get proposal type emit FinalisedProposal(msg.sender, _proposalID, mapPID_votes[_proposalID], _DAOVAULT.totalWeight(), _typeStr); mapPID_votes[_proposalID] = 0; // Reset proposal votes to 0 mapPID_finalised[_proposalID] = true; // Finalise the proposal mapPID_finalising[_proposalID] = false; // Remove proposal from 'finalising' stage mapPID_open[_proposalID] = false; // Close the proposal }
Allow multiple simultaneous proposals Make it easier to cancel proposals Require a stake to enter proposals (which might be slashed)
#0 - SamusElderg
2021-07-23T06:23:18Z
Duplicate of #43 Worth noting the differences though; also the severity is quite low due to being able to proactively or reactively change the DaoFee
#1 - ghoul-sol
2021-08-08T18:29:00Z
Duplicate of #43
gpersoon
The contract DAO.sol has a function to remove the DEPLOYER, e.g. purgeDeployer() This means that the function with the modifier onlyDAO can no longer be executed Note: the name "onlyDAO" is misleading because it doesn't mean the DAO but the deployer of the DAO
Some functions might still be useful, also when upgrading the DAO contract. These could be:
// https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L137 function purgeDeployer() external onlyDAO { DEPLOYER = address(0); }
modifier onlyDAO() { require(msg.sender == DEPLOYER); _; }
function changeBondingPeriod(uint256 bondingSeconds) external onlyDAO{ bondingPeriodSeconds = bondingSeconds; }
function burnBalance() external onlyDAO returns (bool){
uint256 baseBal = iBEP20(BASE).balanceOf(address(this));
iBASE(BASE).burn(baseBal);
return true;
}
// Can transfer the SPARTA remaining in this contract to a new DAO (If DAO is upgraded) function moveBASEBalance(address newDAO) external onlyDAO { uint256 baseBal = iBEP20(BASE).balanceOf(address(this)); iBEP20(BASE).transfer(newDAO, baseBal); }
function finaliseProposal() external { .. if(isEqual(_type, 'DAO')){ moveDao(currentProposal); } else if (isEqual(_type, 'ROUTER')) { moveRouter(currentProposal); } else if (isEqual(_type, 'UTILS')){ moveUtils(currentProposal); } else if (isEqual(_type, 'RESERVE')){ moveReserve(currentProposal); } else if (isEqual(_type, 'FLIP_EMISSIONS')){ flipEmissions(currentProposal); } else if (isEqual(_type, 'COOL_OFF')){ changeCooloff(currentProposal); } else if (isEqual(_type, 'ERAS_TO_EARN')){ changeEras(currentProposal); } else if (isEqual(_type, 'GRANT')){ grantFunds(currentProposal); } else if (isEqual(_type, 'GET_SPARTA')){ _increaseSpartaAllocation(currentProposal); } else if (isEqual(_type, 'LIST_BOND')){ _listBondingAsset(currentProposal); } else if (isEqual(_type, 'DELIST_BOND')){ _delistBondingAsset(currentProposal); } else if (isEqual(_type, 'ADD_CURATED_POOL')){ _addCuratedPool(currentProposal); } else if (isEqual(_type, 'REMOVE_CURATED_POOL')){ _removeCuratedPool(currentProposal);
Double check if there are additional functions that should be able to be executed by the DAO. If so, add them to finaliseProposal()
Rename the modifier onlyDAO to onlyDEPLOYER
#0 - SamusElderg
2021-07-23T08:51:22Z
duplicate of #172
🌟 Selected for report: gpersoon
1182.6017 USDC - $1,182.60
gpersoon
When the DAO is just deployed it is relatively easy to gain a large (majority) share, by depositing a lot in the DAOVault and/of BONDVault. Then you could submit a proposal and vote it in. Luckily there is a coolOffPeriod of 3 days. But if others are not paying attention in these 3 days you might get your vote passed by voting for it with your majority share. The riskiest proposal would be to replace the DAO (moveDao), because that way you could take over everything.
https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol function finaliseProposal() external { require((block.timestamp - mapPID_coolOffTime[currentProposal]) > coolOffPeriod, "!cooloff"); // Must be past cooloff period require(mapPID_finalising[currentProposal] == true, "!finalising"); // Must be in finalising stage if(!hasQuorum(currentProposal)){ mapPID_finalising[currentProposal] = false; // If proposal has lost quorum consensus; kick it out of the finalising stage } else { bytes memory _type = bytes(mapPID_type[currentProposal]); // Get the proposal type if(isEqual(_type, 'DAO')){ moveDao(currentProposal); ...
function voteProposal() external returns (uint voteWeight) { require(mapPID_open[currentProposal] == true, "!open"); // Proposal must be open status bytes memory _type = bytes(mapPID_type[currentProposal]); // Get the proposal type voteWeight = countVotes(); // Vote for proposal and recount if(hasQuorum(currentProposal) && mapPID_finalising[currentProposal] == false){ if(isEqual(_type, 'DAO') || isEqual(_type, 'UTILS') || isEqual(_type, 'RESERVE') ||isEqual(_type, 'GET_SPARTA') || isEqual(_type, 'ROUTER') || isEqual(_type, 'LIST_BOND')|| isEqual(_type, 'GRANT')|| isEqual(_type, 'ADD_CURATED_POOL')){ if(hasMajority(currentProposal)){ _finalise(); // Critical proposals require 'majority' consensus to enter finalization phase } } else { _finalise(); // Other proposals require 'quorum' consensus to enter finalization phase } } emit NewVote(msg.sender, currentProposal, voteWeight, mapPID_votes[currentProposal], string(_type)); }
function hasMajority(uint _proposalID) public view returns(bool){ uint votes = mapPID_votes[_proposalID]; // Get the proposal's total voting weight uint _totalWeight = _DAOVAULT.totalWeight() + _BONDVAULT.totalWeight(); // Get combined total vault weights uint consensus = _totalWeight * majorityFactor / 10000; // Majority > 66.6% if(votes > consensus){ return true; } else { return false; } }
// Check if a proposal has Quorum consensus function hasQuorum(uint _proposalID) public view returns(bool){ uint votes = mapPID_votes[_proposalID]; // Get the proposal's total voting weight uint _totalWeight = _DAOVAULT.totalWeight() + _BONDVAULT.totalWeight(); // Get combined total vault weights uint consensus = _totalWeight / 2; // Quorum > 50% if(votes > consensus){ return true; } else { return false; } }
Pay attention to the proposals when the DAO is just deployed. Make sure you initially have a majority vote.
#0 - verifyfirst
2021-07-23T06:24:42Z
The recommended mitigation steps were already going to be in place for mainNet including emissions etc.
🌟 Selected for report: gpersoon
1182.6017 USDC - $1,182.60
gpersoon
When the DAO is upgraded via moveDao, it also updates the DAO address in BASE. However it doesn't update the DAO address in the Reserve.sol contract. This could be done with the function setIncentiveAddresses(..)
Now the next time grantFunds of DAO.sol is called, its tries to call: _RESERVE.grantFunds(...)
The grantFunds of Reserve.sol has the modifier onlyGrantor(), which checks the msg.sender == DAO. However in the mean time the DAO has been updated and Reserve.sol doesn't know about it and thus the modifier will not allow access to the function. Thus grantFunds will revert.
https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L452 function moveDao(uint _proposalID) internal { address _proposedAddress = mapPID_address[_proposalID]; // Get the proposed new address require(_proposedAddress != address(0), "!address"); // Proposed address must be valid DAO = _proposedAddress; // Change the DAO to point to the new DAO address iBASE(BASE).changeDAO(_proposedAddress); // Change the BASE contract to point to the new DAO address daoHasMoved = true; // Set status of this old DAO completeProposal(_proposalID); // Finalise the proposal }
function grantFunds(uint _proposalID) internal { uint256 _proposedAmount = mapPID_param[_proposalID]; // Get the proposed SPARTA grant amount address _proposedAddress = mapPID_address[_proposalID]; // Get the proposed SPARTA grant recipient require(_proposedAmount != 0, "!param"); // Proposed grant amount must be valid require(_proposedAddress != address(0), "!address"); // Proposed recipient must be valid _RESERVE.grantFunds(_proposedAmount, _proposedAddress); // Grant the funds to the recipient completeProposal(_proposalID); // Finalise the proposal }
// https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/outside-scope/Reserve.sol#L17 modifier onlyGrantor() { require(msg.sender == DAO || msg.sender == ROUTER || msg.sender == DEPLOYER || msg.sender == LEND || msg.sender == SYNTHVAULT, "!DAO"); _; }
function grantFunds(uint amount, address to) external onlyGrantor { .... }
function setIncentiveAddresses(address _router, address _lend, address _synthVault, address _Dao) external onlyGrantor { ROUTER = _router; LEND = _lend; SYNTHVAULT = _synthVault; DAO = _Dao; }
Call setIncentiveAddresses(..) when a DAO upgrade is done.
#0 - verifyfirst
2021-07-23T06:30:41Z
Non critical, however this will be implemented to future proof the protocol
319.3025 USDC - $319.30
gpersoon
The function curatedPoolCount() contains a for loop over the array arrayPools. If arrayPools would be too big then the loop would run out of gas and curatedPoolCount() would revert. This would mean that addCuratedPool() cannot be executed anymore (because it calls curatedPoolCount() )
The array arrayPools can be increased in size arbitrarily by repeatedly doing the following:
//https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/poolFactory.sol#L45 function createPoolADD(uint256 inputBase, uint256 inputToken, address token) external payable returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token require((inputToken > 0 && inputBase >= (10000*10**18)), "!min"); // User must add at least 10,000 SPARTA liquidity & ratio must be finite Pool newPool; address _token = token; if(token == address(0)){_token = WBNB;} // Handle BNB -> WBNB require(_token != BASE && iBEP20(_token).decimals() == 18); // Token must not be SPARTA & it's decimals must be 18 newPool = new Pool(BASE, _token); // Deploy new pool pool = address(newPool); // Get address of new pool mapToken_Pool[_token] = pool; // Record the new pool address in PoolFactory _handleTransferIn(BASE, inputBase, pool); // Transfer SPARTA liquidity to new pool _handleTransferIn(token, inputToken, pool); // Transfer TOKEN liquidity to new pool arrayPools.push(pool); // Add pool address to the pool array ..
function curatedPoolCount() internal view returns (uint){ uint cPoolCount; for(uint i = 0; i< arrayPools.length; i++){ if(isCuratedPool[arrayPools[i]] == true){ cPoolCount += 1; } } return cPoolCount; }
function addCuratedPool(address token) external onlyDAO { ... require(curatedPoolCount() < curatedPoolSize, "maxCurated"); // Must be room in the Curated list
//https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Pool.sol#L187 function remove() external returns (uint outputBase, uint outputToken) { return removeForMember(msg.sender); }
// Contract removes liquidity for the user function removeForMember(address member) public returns (uint outputBase, uint outputToken) { uint256 _actualInputUnits = balanceOf(address(this)); // Get the received LP units amount outputBase = iUTILS(_DAO().UTILS()).calcLiquidityHoldings(_actualInputUnits, BASE, address(this)); // Get the SPARTA value of LP units outputToken = iUTILS(_DAO().UTILS()).calcLiquidityHoldings(_actualInputUnits, TOKEN, address(this)); // Get the TOKEN value of LP units _decrementPoolBalances(outputBase, outputToken); // Update recorded BASE and TOKEN amounts _burn(address(this), _actualInputUnits); // Burn the LP tokens iBEP20(BASE).transfer(member, outputBase); // Transfer the SPARTA to user iBEP20(TOKEN).transfer(member, outputToken); // Transfer the TOKENs to user emit RemoveLiquidity(member, outputBase, outputToken, _actualInputUnits); return (outputBase, outputToken); }
Create a variable curatedPoolCount and increase it in addCuratedPool and decrease it in removeCuratedPool
#0 - verifyfirst
2021-07-23T06:13:22Z
We agree with the issue, this could be more efficient.
🌟 Selected for report: gpersoon
394.2006 USDC - $394.20
gpersoon
The function depositForMember of BondVault.sol adds user to the array arrayMembers. However it does this for each asset that a user deposits. Suppose a user deposit multiple assets, than the user is added multiple times to the array arrayMembers.
This will mean the memberCount() doesn't show accurate results. Also allMembers() will contain duplicate members
// https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/BondVault.sol#L60 function depositForMember(address asset, address member, uint LPS) external onlyDAO returns(bool){ if(!mapBondAsset_memberDetails[asset].isMember[member]){ mapBondAsset_memberDetails[asset].isMember[member] = true; // Register user as member (scope: user -> asset) arrayMembers.push(member); // Add user to member array (scope: vault) mapBondAsset_memberDetails[asset].members.push(member); // Add user to member array (scope: user -> asset) } ...
// Get the total count of all existing & past BondVault members function memberCount() external view returns (uint256 count){ return arrayMembers.length; } function allMembers() external view returns (address[] memory _allMembers){ return arrayMembers; }
Use a construction like this: mapping(address => bool) isMember; if(!isMember[member]){ isMember[member] = true; arrayMembers.push(member); }
#0 - SamusElderg
2021-07-23T09:45:49Z
This appears to be true 👍 Will need to have some discussion around whether it is worth the extra gas for the extra check when adding the member. My limited opinion is that it is worth the extra gas to add the extra conditional for this one and have counts lining up to the correct amount even if it isn't used elsewhere. But @verifyfirst ill let you decide on that!
#1 - ghoul-sol
2021-08-08T20:21:15Z
Sponsor confirmed so I'm keeping this
🌟 Selected for report: gpersoon
394.2006 USDC - $394.20
gpersoon
The function getPool doesn't check if the pool exits (e.g. it doesn't check if the resulting pool !=0) Other functions use the results of getPool and do followup actions.
For example createSynth checks isCuratedPool(_pool) == true; if somehow isCuratedPool(0) would set to be true, then further actions could be done. As far as I can see no actual problem occurs, but this is a dangerous construction and future code changes could introduce vulnerabilities. Additionally the reverts that will occur if the result of getPool==0 are perhaps difficult to troubleshoot.
https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/poolFactory.sol#L119 function getPool(address token) public view returns(address pool){ if(token == address(0)){ pool = mapToken_Pool[WBNB]; // Handle BNB } else { pool = mapToken_Pool[token]; // Handle normal token } return pool; }
function createPoolADD(uint256 inputBase, uint256 inputToken, address token) external payable returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token
function createPool(address token) external onlyDAO returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token
// https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/synthFactory.sol#L37 function createSynth(address token) external returns(address synth){ require(getSynth(token) == address(0), "exists"); // Synth must not already exist address _pool = iPOOLFACTORY(_DAO().POOLFACTORY()).getPool(token); // Get pool address require(iPOOLFACTORY(_DAO().POOLFACTORY()).isCuratedPool(_pool) == true, "!curated"); // Pool must be Curated
In function getPool add something like: require (pool !=0, "Pool doesn't exist");
Note: the functions createPoolADD and createPool also have to be changed, to use a different way to verify the pool doesn't exist.
106.4342 USDC - $106.43
gpersoon
The function createPoolADD() supports the input of BNB, which it detects by checking "token == address(0)" Later it calls "_handleTransferIn(token, ...);" with the original value of token, which can be 0.
However in the function _handleTransferIn() poolFactory.sol there is no provision to transfer BNB (it doesn't check for _token == 0), so it will revert when you try to add BNB.
As a comparison, the function _handleTransferIn() of Router.sol does check for _token == address(0) and takes appropriate action.
//https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/poolFactory.sol#L45 function createPoolADD(uint256 inputBase, uint256 inputToken, address token) external payable returns(address pool){ ... address _token = token; if(token == address(0)){_token = WBNB;} // Handle BNB -> WBNB ... _handleTransferIn(token, inputToken, pool); // Transfer TOKEN liquidity to new pool
function _handleTransferIn(address _token, uint256 _amount, address _pool) internal returns(uint256 actual){ if(_amount > 0) { uint startBal = iBEP20(_token).balanceOf(_pool); iBEP20(_token).transferFrom(msg.sender, _pool, _amount); actual = iBEP20(_token).balanceOf(_pool) - (startBal); } }
//https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Router.sol#L197 function _handleTransferIn(address _token, uint256 _amount, address _pool) internal returns(uint256 actual){ if(_amount > 0) { if(_token == address(0)){ require((_amount == msg.value)); (bool success, ) = payable(WBNB).call{value: _amount}(""); // Wrap BNB require(success, "!send"); iBEP20(WBNB).transfer(_pool, _amount); // Transfer WBNB from ROUTER to pool actual = _amount; } else { uint startBal = iBEP20(_token).balanceOf(_pool); // Get prior TOKEN balance of pool iBEP20(_token).transferFrom(msg.sender, _pool, _amount); // Transfer TOKEN to pool actual = iBEP20(_token).balanceOf(_pool)-(startBal); // Get received TOKEN amount } } }
Apply the same function as _handleTransferIn of Router.sol to _handleTransferIn of poolFactory.sol Better yet deduplicate the function by moving it to a library/included solidity file.
Note: There is also a _handleTransferIn in Synth.sol which isn't used.
#0 - SamusElderg
2021-07-22T03:14:36Z
Whilst true; the intention is always that BNB will already be listed as standard; so the user's createPoolADD() function is irrelevant to BNB. However, this was not made clear anywhere; so is a good observation! @verifyfirst should we leave this? Or Block BNB pool thru that function? Or adjust the function to account for BNB even though it will already be listed?
#1 - SamusElderg
2021-07-23T06:19:05Z
No need for action on this one; BNB pool will be deployed at the same time.
#2 - ghoul-sol
2021-08-08T21:01:17Z
Per sponsor comment, low risk
🌟 Selected for report: gpersoon
160 USDC - $160.00
gpersoon
Sometimes the reference to function calls, that are done via the DAO, are looked up multiple times in one function call. For example mintSynth calls: ​
This can be done more efficient by caching the result of _DAO() and _DAO().UTILS()
f## Proof of Concept // https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Pool.sol#L229 ​function mintSynth(address synthOut, address member) external returns(uint outputAmount, uint fee) { ​require(iSYNTHFACTORY(_DAO().SYNTHFACTORY()).isSynth(synthOut) == true, "!synth"); // Must be a valid Synth ​.. ​uint output = iUTILS(_DAO().UTILS()).calcSwapOutput(_actualInputBase, baseAmount, tokenAmount); // Calculate value of swapping SPARTA to the relevant underlying TOKEN ​uint _liquidityUnits = iUTILS(_DAO().UTILS()).calcLiquidityUnitsAsym(_actualInputBase, address(this)); // Calculate LP tokens to be minted ​.. ​uint _fee = iUTILS(_DAO().UTILS()).calcSwapFee(_actualInputBase, baseAmount, tokenAmount); // Calc slip fee in TOKEN ​fee = iUTILS(_DAO().UTILS()).calcSpotValueInBase(TOKEN, _fee); // Convert TOKEN fee to SPARTA ​
function _DAO() internal view returns(iDAO) { ​return iBASE(BASE).DAO(); ​}
//https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L624 ​function UTILS() public view returns(iUTILS){ ​if(daoHasMoved){ ​return Dao(DAO).UTILS(); ​} else { ​return _UTILS; ​} ​}
Cache _DAO() and cache the sub functions like: _DAO().UTILS()) If called multiple times from function