Benvenuti alla seconda parte di questo workshop di sviluppo! Assicurati di leggere la prima parte (qui) poiché la parte 2 inizia dove è finita la parte I.
Un piccolo promemoria della parte 1
Friend.tech opera come una piattaforma decentralizzata basata su token sociali lanciata nell’agosto 2023 sulla blockchain Base. Consente ai suoi utenti di scambiare “chiavi”, precedentemente chiamate “azioni”, associate a X (ex Twitter) profili. Il possesso di queste chiavi garantisce l’accesso a chat room esclusive e contenuti speciali dal rispettivo titolare dell’account X.
La piattaforma si promuove dicendo: “Fai trading con i tuoi amici nel nostro mercato”. I fan possono acquistare e vendere azioni, con il prezzo direttamente legato al numero di azioni in circolazione. Ciò crea un’opportunità per i primi sostenitori di trarre profitto man mano che la base di fan dell’individuo cresce, e il protocollo e il soggetto riscuotono anche commissioni su ogni operazione che avviene sulle azioni. Si tratta di un esperimento interessante nella narrativa del trend dei token sociali.
Nella sessione precedente, sia la tariffa del protocollo che la percentuale della commissione del soggetto sono state implementate con successo, insieme alle funzioni necessarie per impostarle. Inoltre, è stata introdotta la funzione di proprietà e un meccanismo per mantenere l’equilibrio di ciascun detentore per un soggetto, oltre a tenere traccia del numero totale di azioni in circolazione per quel soggetto. Invece di fare affidamento sulla struttura dei dati di mappatura come in Solidity, per raggiungere questo obiettivo sono stati utilizzati i subappalti di Ralph.
È stata creata anche la funzione “get price” che calcola il prezzo delle azioni, considerando sia la commissione del protocollo che la commissione dell’oggetto. Questa funzione funge da base per determinare i prezzi di acquisto e di vendita. Questa sessione si concentra sull’implementazione delle funzioni di acquisto e vendita di azioni. Se non l’hai ancora fatto, ecco cosa ti serve per preparare il tuo ambiente.
Implementazione di Ralph
Puoi seguire il workshop completo e dettagliato nel seguente video. Di seguito è riportato il codice completo dell’implementazione di Ralph e questo articolo evidenzia i passaggi principali per una comprensione più semplice:
Ricapitolando: Struttura del subappalto
Nella sessione precedente sono stati creati tre contratti essenziali. Il primo è il contratto “Friend.tech”, che è il contratto primario. Da lì, fornendo l’indirizzo del soggetto, è stato generato un subappalto, chiamato “SubjectShare”. Questo contratto tiene traccia del saldo totale specifico per il soggetto designato.
Successivamente, dopo aver ottenuto l’indirizzo del titolare, è stato creato un altro subappalto denominato “SubjectShareBalance”. Questo subappalto è responsabile del monitoraggio del saldo del detentore in relazione all’oggetto in questione. Questa struttura gerarchica racchiude il nostro sistema di subappalto.
Funzioni di acquisto e vendita di azioni
Il nostro obiettivo attuale è quello di implementare le funzioni “comprare azioni” e “vendere azioni” all’interno del contratto “Friend.tech”. L’approccio prevede di procedere passo dopo passo dal basso verso l’alto sulla struttura del subappalto.
Contratto “SubjectShareBalances”
Il nostro obiettivo iniziale risiede nella creazione di una funzione responsabile dell’aggiornamento del saldo all’interno di “SubjectShareBalances”. Esaminando il contratto, notiamo che contiene due funzioni: “aggiungi saldo” e “riduci saldo”.
@using(updateFields = true)
pub fn addBalance(amount: U256) -> () {
checkCaller!(callerContractId!() == subjectSharesContractId, ErrorCodes.SubjectSharesContractAllowedOnly)
balance = balance + amount
}
@using(updateFields = true, assetsInContract = true)
pub fn reduceBalance(amount: U256) -> () {
checkCaller!(callerContractId!() == subjectSharesContractId, ErrorCodes.SubjectSharesContractAllowedOnly)
assert!(balance >= amount, ErrorCodes.NotEnoughBalance)
balance = balance - amount
if (balance == 0) {
destroySelf!(holder)
}
}
Ci sono cose interessanti qui: ci sono precauzioni per limitare l’accesso a queste funzioni. Solo il contratto padre, che è “Quota oggetto”, ha l’autorità di chiamare queste funzioni e di effettuare aggiornamenti del saldo. A tale scopo, è necessario fornire l’ID contratto corretto quando si richiamano queste funzioni. Qualsiasi tentativo da parte di chiamanti non autorizzati comporterà la generazione di un’eccezione. Una volta verificata la correttezza del chiamante, procediamo ad aggiornare il saldo di conseguenza.
Un’altra caratteristica degna di nota è che quando il saldo raggiunge lo zero, questo contratto si autodistrugge. Restituisce il deposito del contratto al titolare che ha inizialmente creato questo subappalto. Questo dimostra uno dei vantaggi dell’utilizzo dei subappalti rispetto alle mappature: ci incentiva a rimuovere i contratti non necessari dalla blockchain, impedendo loro di persistere a tempo indeterminato.
Con queste funzioni di aggiornamento del bilanciamento, il nostro prossimo passo è spostare la nostra attenzione un po’ più in alto nella gerarchia e procedere con l’implementazione delle funzioni “acquista” e “vendi” a questo livello.
Contratto “SubjectShares”
@using(preapprovedAssets = true, assetsInContract = true, updateFields = true)
pub fn buy(holder: Address, amount: U256, subjectFee: U256) -> () {
checkCaller!(callerContractId!() == friendContractId, ErrorCodes.FriendContractAllowedOnly)
supply = supply + amount
if (holder == subject) {
subjectOwnBalance = subjectOwnBalance + amount
} else {
let subjectSharesBalanceContractId = subContractId!(toByteVec!(holder))
if (contractExists!(subjectSharesBalanceContractId)) {
SubjectSharesBalance(subjectSharesBalanceContractId).addBalance(amount)
} else {
let (encodeImmutableFields, encodeMutableFields) = SubjectSharesBalance.encodeFields!(holder, selfContractId!(), amount)
copyCreateSubContract!{holder -> ALPH: 1 alph}(
toByteVec!(holder),
subjectSharesBalanceTemplateId,
encodeImmutableFields,
encodeMutableFields
)
}
}
Esaminiamo il contratto “SubjectShares”, concentrandoci sulle sue funzioni di “acquisto” e “vendita”. La funzione “buy” accetta tre parametri: l’indirizzo dell’acquirente, la quantità di azioni desiderata e la commissione oggetto, che deve essere pagata quando si acquistano o vendono azioni al soggetto.
Analogamente al contratto “SubjectSharesBalance”, è necessario controllare l’accesso alla funzione. In questo contesto, solo il contratto padre, ovvero il contratto “Friend.tech”, può chiamare questo metodo. A tale scopo, l’ID del contratto “Friend.tech” viene passato come parametro, assicurando che solo esso possa richiamare la funzione; In caso contrario, verrà generato un errore.
Dopo aver confermato l’identità del chiamante, l’offerta totale di azioni di questo soggetto viene aggiornata. Il codice verifica quindi se l’acquirente è il soggetto stesso. In questo caso, la funzione aggiorna direttamente il campo del saldo del proprietario del soggetto, ignorando la necessità di interagire con i subappalti.
Se invece l’acquirente è un altro soggetto, il codice procede a verificare se esiste già un subappalto per tale titolare. Se ne esiste uno, indica che questo titolare possiede già alcune azioni. Al contrario, se non viene trovato alcun subappalto, suggerisce che il detentore non ha azioni associate a questo soggetto. A tal fine, l’indirizzo del titolare viene utilizzato come chiave per recuperare l’ID del contratto di subappalto esistente. Successivamente, la funzione “contratto esiste” viene impiegata per confermarne l’esistenza.
} else {
let (encodeImmutableFields, encodeMutableFields) = SubjectSharesBalance.encodeFields!(holder, selfContractId!(), amount)
copyCreateSubContract!{holder -> ALPH: 1 alph}(
toByteVec!(holder),
subjectSharesBalanceTemplateId,
encodeImmutableFields,
encodeMutableFields
)
}
Se il conto lavoro non esiste, viene creato. A tale scopo, viene utilizzata la funzione “copyCreateSubContract”, che è considerata più efficiente della funzione alternativa “createSubContract!” poiché non richiede la fornitura di codice aggiuntivo. Utilizza invece l’indirizzo del titolare come chiave e inizializza i campi del subappalto, che includono il titolare, l’ID del contratto di condivisione del soggetto e il saldo iniziale. In questo modo si garantisce che il saldo del titolare per questo soggetto sia inizializzato in modo appropriato. Successivamente, si può prendere in considerazione la creazione di questo subappalto.
Tuttavia, se il conto lavoro esiste già, il processo prevede l’invocazione della funzione “aggiungi saldo” creata di recente dal commit precedente per aggiornare il saldo. Una volta che il saldo è stato affrontato, il passo successivo è quello di trasferire il canone in oggetto al particolare contratto in questione.
}
@using(preapprovedAssets = true, assetsInContract = true, updateFields = true)
pub fn sell(seller: Address, amount: U256, subjectFee: U256) -> () {
checkCaller!(callerContractId!() == friendContractId, ErrorCodes.FriendContractAllowedOnly)
assert!(supply > amount, ErrorCodes.NotEnoughBalance)
supply = supply - amount
let subjectSharesBalanceContractId = subContractId!(toByteVec!(seller))
let subjectSharesBalanceContract = SubjectSharesBalance(subjectSharesBalanceContractId)
subjectSharesBalanceContract.reduceBalance(amount)
transferToken!(callerAddress!(), selfAddress!(), ALPH, subjectFee)
}
Quando si vendono azioni, il processo è essenzialmente inverso. Innanzitutto, c’è la conferma di avere un equilibrio adeguato. Quindi, la funzione “subContractId!” viene utilizzata per ottenere l’ID contratto del contratto di saldo della quota in oggetto. Successivamente, viene chiamata la funzione “riduci equilibrio”. Se questo contratto non esiste, l’intera operazione non avrà esito positivo. Una volta effettuata la chiamata, il saldo viene ridotto e il canone dell’oggetto viene trasferito al contratto.
In sintesi, sia la funzione di acquisto che quella di vendita hanno due scopi principali: gestire l’equilibrio per il detentore e regolare l’offerta totale per il soggetto. Inoltre, facilitano il trasferimento della commissione in oggetto al contratto di partecipazione in oggetto.
Friend.tech smart contract
@using(checkExternalCaller = false, assetsInContract = true, preapprovedAssets = true, updateFields = true)
pub fn buyShares(sharesSubject: Address, amount: U256) -> () {
let supply = getSupply(sharesSubject)
let buyer = callerAddress!()
assert!(supply > 0 || buyer == sharesSubject, ErrorCodes.SubjectAllowedFirstShareOnly)
let price = getPrice(supply, amount)
let protocolFee = price * protocolFeePercent / 10000
let subjectFee = price * subjectFeePercent / 10000
let subjectSharesContractId = subContractId!(toByteVec!(sharesSubject))
if (contractExists!(subjectSharesContractId)) {
let balance = SubjectShares(subjectSharesContractId).getBalance(buyer)
let mut approvedAssets = subjectFee
if (balance == 0) {
// SubjectSharesBalance contract deposit
approvedAssets = approvedAssets + 1 alph
}
SubjectShares(subjectSharesContractId).buy{buyer -> ALPH: approvedAssets}(buyer, amount, subjectFee)
Il passo successivo prevede lo spostamento verso l’alto della gerarchia e l’implementazione delle funzioni “compra azioni” e “vendi azioni” all’interno dello smart contract “Friend.tech”. Queste funzioni sono evidenziate come i prossimi punti focali dello sviluppo.
Innanzitutto, esploriamo la funzione “compra azione”. Il suo obiettivo primario è quello di determinare l’offerta per il soggetto. Il contratto “friend.tech” applica una regola che impone agli individui di acquistare azioni iniziali da soli se l’offerta è pari a zero. Di conseguenza, se l’offerta è effettivamente nulla, l’acquirente deve esserne il soggetto stesso, allineandosi alla logica di business stabilita.
Successivamente, il codice procede a calcolare il prezzo dell’azione in base a fattori quali l’offerta, il numero totale di azioni in circolazione e la quantità di azioni che l’acquirente intende acquistare. Successivamente, calcola sia il protocollo che le tariffe dell’oggetto. Il passo successivo consiste nel determinare l’esistenza del contratto di subappalto “SubjectShares”.
Questa valutazione si concentra sull’esistenza dell’indirizzo dell’argomento, in quanto l’obiettivo è quello di acquisire azioni per l’argomento specifico in questione. Se il contratto azionario in oggetto esiste, significa l’emissione di azioni in circolazione. Se non esiste, indica che non sono mai state emesse azioni da questo soggetto. In tal caso, l’emittente delle azioni deve essere il soggetto stesso.
Dopo tale verifica, qualora esista il contratto di quota in oggetto, il codice procede al pagamento della quota in oggetto, come precedentemente stabilito. Inizialmente, tenta di approvare l’importo della commissione in oggetto. Se il saldo è pari a zero in questa fase, significa che l’acquirente, nonostante le azioni in circolazione per questo soggetto, non possiede azioni associate a questo soggetto.
In questo scenario, se si tenta di aggiornare il saldo e richiede la creazione di un subappalto per l’acquirente, ciò implica che verrà detratta una parte del deposito del contratto. Questo spiega perché, in caso di saldo zero, è necessario approvare un importo leggermente maggiore, nello specifico un ALPH in più, come deposito del contratto, per consentire la creazione del subappalto.
Successivamente, una volta ottenuta la quantità richiesta di asset approvati, il codice invoca la funzione “buy”, fornendogli l’importo dell’asset approvato in ALPH.
if (balance == 0) {
// SubjectSharesBalance contract deposit
approvedAssets = approvedAssets + 1 alph
}
SubjectShares(subjectSharesContractId).buy{buyer -> ALPH: approvedAssets}(buyer, amount, subjectFee)
Vale la pena sottolineare qui come questo sottolinei l’efficacia del sistema di autorizzazione degli asset. Indipendentemente dal bilanciatore iniziale dell’acquirente, questa particolare riga di codice, racchiusa tra parentesi quadre, limita rigorosamente la spesa nell’ambito della funzione. Applica un controllo preciso sulla quantità di ALPH che può essere spesa, dimostrando la potenza dell’Asset Permission System.
} else {
let (encodeImmutableFields, encodeMutableFields) = SubjectShares.encodeFields!(
subjectSharesBalanceTemplateId,
buyer,
selfContractId!(),
amount,
amount
)
copyCreateSubContract!{buyer -> ALPH: subjectFee + 1 alph}(
toByteVec!(sharesSubject),
subjectSharesTemplateId,
encodeImmutableFields,
encodeMutableFields
)
}
totalProtocolFee = totalProtocolFee + protocolFee
transferToken!(buyer, selfAddress!(), ALPH, price + protocolFee)
emit Trade(buyer, sharesSubject, true, amount, price, protocolFee, subjectFee, supply + amount)
}
Al contrario, quando questo subappalto è assente, implica che nessuno ha precedentemente acquisito azioni per questo particolare soggetto. In tal caso, anche l’acquirente deve essere il soggetto stesso. Di conseguenza, viene stabilito un nuovo subappalto per il soggetto e vengono configurati i parametri essenziali.
La creazione di un nuovo contratto di condivisione soggetto viene inizializzata con valori cruciali per il conto lavoro. Il saldo del soggetto è impostato sull’importo iniziale, che rappresenta l’offerta totale. Inoltre, viene configurato l’ID del contratto principale, assicurando che l’acquirente sia allineato con il soggetto. Di conseguenza, l’indirizzo viene impostato su quello dell’acquirente.
In sostanza, la creazione di un nuovo contratto di condivisione del soggetto ha lo scopo di monitorare l’equilibrio complessivo del soggetto. Gestisce in modo efficiente il saldo e facilita il pagamento della commissione al soggetto. Successivamente, viene pagata la quota di protocollo, nonché il prezzo dell’azione. L’importo totale richiesto all’acquirente per questo contratto include il prezzo dell’azione e la commissione del protocollo, entrambi aggiornati di conseguenza. Mentre viene mantenuta una contabilità separata per la commissione del protocollo, l’importo totale copre il prezzo delle azioni e la commissione del protocollo combinati. A seguito di queste azioni, viene emesso un evento che indica il completamento della funzione “acquista”.
@using(checkExternalCaller = false, assetsInContract = true, updateFields = true)
pub fn sellShares(sharesSubject: Address, amount: U256) -> () {
let seller = callerAddress!()
let subjectSharesContractId = subContractId!(toByteVec!(sharesSubject))
assert!(contractExists!(subjectSharesContractId), ErrorCodes.NoShareForTheSubject)
let subjectShares = SubjectShares(subjectSharesContractId)
let supply = subjectShares.getSupply()
assert!(supply > amount, ErrorCodes.CanNotSellLastShare)
let price = getPrice(supply - amount, amount)
let protocolFee = price * protocolFeePercent / 10000
let subjectFee = price * subjectFeePercent / 10000
assert!(subjectShares.getBalance(seller) >= amount, ErrorCodes.InsufficientShares)
totalProtocolFee = totalProtocolFee + protocolFee
transferTokenFromSelf!(seller, ALPH, price - subjectFee - protocolFee)
subjectShares.sell{selfAddress!() -> ALPH : subjectFee}(seller, amount, subjectFee)
emit Trade(seller, sharesSubject, false, amount, price, protocolFee, subjectFee, supply - amount)
}
La funzione “vendere azioni” funziona in modo opposto alla funzione “acquistare”. Inizia verificando due condizioni: in primo luogo, devono esserci azioni disponibili per il soggetto specifico e, in secondo luogo, devono esserci azioni sufficienti possedute dal venditore. Questo processo di verifica è essenziale prima di procedere.
Successivamente, il codice calcola il prezzo di vendita, considerando il protocollo e le commissioni del soggetto. Quindi procede a pagare al venditore questo prezzo calcolato. Il prezzo di vendita è determinato deducendo sia la commissione oggetto che la commissione di protocollo, con conseguente ricavo netto ricevuto dal venditore.
totalProtocolFee = totalProtocolFee + protocolFee
transferTokenFromSelf!(seller, ALPH, price - subjectFee - protocolFee)
subjectShares.sell{selfAddress!() -> ALPH : subjectFee}(seller, amount, subjectFee)
emit Trade(seller, sharesSubject, false, amount, price, protocolFee, subjectFee, supply - amount)
}
La tariffa di protocollo pagata all’interno dello smart contract Friend.tech viene inviata alla tariffa totale del protocollo. Questo processo rappresenta un trasferimento interno di fondi all’interno dello smart contract.
Oltre a ciò, lo smart contract, poiché deduce la commissione dell’oggetto dal pagamento totale, paga la commissione dell’oggetto al soggetto. Questo è raffigurato nel frammento di codice, in cui i fondi vengono trasferiti dall’indirizzo autonomo (che è il protocollo stesso) per coprire la commissione dell’oggetto. Questa operazione è un altro esempio delle funzionalità di Asset Permission System. Sebbene lo smart contract Friend.tech abbia accesso a molti asset, tra cui il prezzo delle azioni e le commissioni del protocollo, è approvato solo per spendere l’importo esatto della commissione oggetto nell’ambito della funzione “vendi”. Ciò garantisce la corretta gestione delle commissioni in oggetto e mantiene l’integrità del saldo all’interno dei subappalti, vale a dire i contratti “subjectShare” e “subjectShareBalance”. A seguito dell’esecuzione di queste azioni, viene emesso un evento di trading per segnare il completamento della funzione “vendere azioni”.
Test e riepilogo
Nel repository GitHub sono previsti diversi test per verificare se l’implementazione funziona come previsto, in particolare tutta la matematica relativa al calcolo del prezzo e alla detrazione delle commissioni.
Questo si conclude con questo articolo, in cui approfondiamo l’implementazione delle funzioni di acquisto e vendita di azioni, che comporteranno anche l’aggiornamento dei saldi, creando e distruggendo così i subappalti secondo necessità. Anche l’Asset Permission System è stato visto in azione, ed è un potente strumento per la gestione dei trasferimenti di asset.
Facci sapere se hai domande su Twitter, Discord, Telegram o Reddit!