Sparta
Sparta is an undercollaterized credit pool based on AkropolisOS, where members of which can earn high-interest rates by providing undercollateralized loans to other members and by pooling and investing capital through various liquid DeFi instruments.
Github repo:
Mainnet deployment
DAI:
0x6b175474e89094c44da98b954eedeac495271d0fPool:
0x73067fdd366Cb678E9b539788F4C0f34C5700246AccessModule:
0xfE7B0aeb84D134c5be6b217e51B2b040F5B7cB7BPToken:
0xAA2edc0E5CDE4Da80628972c501e79326741dB17CurveModule:
0xFb6b0103063CDf701b733db3Fa3F1c0686F19668FundsModule:
0xc88F54A79CaE4C125D7A8c2Cf811daaE78b07D64LiquidityModule:
0x543cBc6693f8cBCf0AE5f2cfd9922203cc13b10ALoanLimitsModule:
0x42b41f636C9eBB150F859f65e3c0f938b0347f59LoanProposalsModule:
0xd3bdEdA5e165E67985a4Dc7927E4651Bedd1950cLoanModule:
0x42E24De51db5baf6E18F91619195375FBAe63b13
Developer tools
Diagrams
Modules

User Interactions

Deployment
Required data:
Address of liquidity token (
LToken.address)
Deployment sequence:
Pool
Deploy proxy and contract instance
Call
initialize()
Liquidity token
Register in pool:
Pool.set("ltoken", LToken.address)
PToken
Deploy proxy and contract instance
Call
initialize(Pool.address)Register in pool:
Pool.set("ptoken", PToken.address)
CurveModule
Deploy proxy and contract instance
Call
initialize(Pool.address)Register in pool:
Pool.set("curve", CurveModule.address)
AccessModule
Deploy proxy and contract instance
Call
initialize(Pool.address)Register in pool:
Pool.set("access", CurveModule.address)
LiquidityModule
Deploy proxy and contract instance
Call
initialize(Pool.address)Register in pool:
Pool.set("liquidity", LiquidityModule.address)
LoanModule, LoanLimitsModule, LoanProposalsModule
Deploy proxy and contract instance of LoanLimitsModule
Call
LoanLimitsModule.initialize(Pool.address)Register in pool:
Pool.set("loan_limits", LoanLimitsModule.address)Deploy proxy and contract instance of LoanProposalsModule
Call
LoanProposalsModule.initialize(Pool.address)Register in pool:
Pool.set("loan_proposals", LoanProposalsModule.address)Deploy proxy and contract instance of LoanModule
Call
LoanModule.initialize(Pool.address)Register in pool:
Pool.set("loan", LoanModule.address)
FundsModule
Deploy proxy and contract instance
Call
initialize(Pool.address)Register in pool:
Pool.set("funds", FundsModule.address)Add LiquidityModule as FundsOperator:
FundsModule.addFundsOperator(LiquidityModule.address)Add LoanModule as FundsOperator:
FundsModule.addFundsOperator(LoanModule.address)Add FundsModule as a Minter for PToken:
PToken.addMinter(FundsModule.address)
Liquidity
Deposit
Required data:
lAmount: Deposit amount, DAI
Required conditions:
All contracts are deployed
Workflow:
Call
FundsModule.calculatePoolEnter(lAmount)to determine expected PTK amount (pAmount)Determine minimum acceptable amount of PTK
pAmountMin <= pAmount, which user expects to get when depositlAmountof DAI. Zero value is allowed.Call
LToken.approve(FundsModule.address, lAmount)to allow exchangeCall
LiquidityModule.deposit(lAmount, pAmountMin)to execute exchange
Withdraw
Required data:
pAmount: Withdraw amount, PTK
Required conditions:
Available liquidity
LToken.balanceOf(FundsModule.address)is greater than expected amount of DAIUser has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
Workflow:
Call
FundsModule.calculatePoolExitInverse(pAmount)to determine expected amount of DAI (lAmount). The response has 3 values, use the second one.Determine minimum acceptable amount
lAmountMin <= lAmountof DAI , which user expects to get when depositpAmountof PTK. Zero value is allowed.Call
PToken.approve(FundsModule.address, pAmount)to allow exchangeCall
LiquidityModule.withdraw(pAmount, lAmountMin)to execute exchange
Credits
Create Loan Request
Required data:
debtLAmount: Loan amount, DAIinterest: Interest rate, percentspAmountMax: Maximal amount of PTK to use as borrower's own pledgedescriptionHash: Hash of loan description stored in Swarm
Required conditions:
User has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
Workflow:
Call
FundsModule.calculatePoolExitInverse(pAmount)to determine expected pledge in DAI (lAmount). The response has 3 values, use the first one.Determine minimum acceptable amount
lAmountMin <= lAmountof DAI, which user expects to lock as a pledge, sendingpAmountof PTK. Zero value is allowed.Call
PToken.approve(FundsModule.address, pAmount)to allow operation.Call
LoanModule.createDebtProposal(debtLAmount, interest, pAmountMax, descriptionHash)to create loan proposal.
Data required for future calls:
Proposal index:
proposalIndexfrom eventDebtProposalCreated.
Add Pledge
Required data:
Loan proposal identifiers:
borrowerAddress of borrowerproposalProposal index
pAmountPledge amount, PTK
Required conditions:
Loan proposal created
Loan proposal not yet executed
Loan proposal is not yet fully filled:
LoanModule.getRequiredPledge(borrower, proposal) > 0User has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
Workflow:
Call
FundsModule.calculatePoolExitInverse(pAmount)to determine expected pledge in DAI (lAmount). The response has 3 values, use the first one.Determine minimum acceptable amount
lAmountMin <= lAmountof DAI, which user expects to lock as a pledge, sendingpAmountof PTK. Zero value is allowed.Call
PToken.approve(FundsModule.address, pAmount)to allow operation.Call
LoanModule.addPledge(borrower, proposal, pAmount, lAmountMin)to execute operation.
Withdraw Pledge
Required data:
Loan proposal identifiers:
borrowerAddress of borrowerproposalProposal index
pAmountAmount to withdraw, PTK
Required conditions:
Loan proposal created
Loan proposal not yet executed
User pledge amount >=
pAmount
Workflow:
Call
LoanModule.withdrawPledge(borrower, proposal, pAmount)to execute operation.
Loan issuance
Required data:
proposal Proposal index
Required conditions:
Loan proposal created, user (transaction sender) is the
borrowerLoan proposal not yet executed
Loan proposal is fully funded:
LoanModule.getRequiredPledge(borrower, proposal) == 0Pool has enough liquidity
Workflow:
Call
LoanModule.executeDebtProposal(proposal)to execute operation.
Data required for future calls:
Loan index:
debtIdxfrom eventDebtProposalExecuted.
Loan repayment (partial or full)
Required data:
debtLoan indexlAmountRepayable amount, DAI
Required conditions:
User (transaction sender) is the borrower
Loan is not yet fully repaid
Workflow:
Call
LToken.approve(FundsModule.address, lAmount)to allow operation.Call
LoanModule.repay(debt, lAmount)to execute operation.
Distributions
When borrower repays some part of his loan, he uses some PTK (either from his balance or minted when he sends DAI to the pool). This PTKs are distributed to supporters, proportionally to the part of the loan they covered. The borrower himself also covered half of the loan, and his part is distributed over the whole pool. All users of the pool receive part of this distributions proportional to the amount of PTK they hold on their balance and in loan proposals, PTK locked as collateral for loans is not counted.

Distribution mechanics
When you need to distribute some amount of tokens over all token holders one's first straight-forward idea might be to iterate through all token holders, check their balance and increase it by their part of the distribution. Unfortunately, this approach can hardly be used in Ethereum blockchain. All operations in EVM cost some gas. If we have a lot of token holders, gas cost for iteration through all may be higher than a gas limit for transaction (which is currently equal to gas limit for block). Instead, during distribution we just store amount of PTK to be distributed and current amount of all PTK qualified for distribution. And user balance is only updated by separate request or when it is going to be changed by transfer, mint or burn. During this "lazy" update we go through all distributions occured between previous and current update. Now, one may ask what if there is too much distributions occurred in the pool between this updated and the gas usage to iterate through all of them is too high again? Obvious solution would be to allow split such transaction to several smaller ones, and we've implemented this approach. But we also decided to aggregate all distributions during a day. This way we can protect ourself from dust attacks, when somebody may do a lot of small repays which cause a lot of small distributions. When a distribution request is received by PToken we check if it's time to actually create new distribution. If it's not, we just add distribution amount to the accumulator. When time comes (and this condition is also checked by transfers, mints and burns), actual distribution is created using accumulated amount of PTK and total supply of qualified PTK.
Last updated