Create an account


Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tut] Solidity Example – Safe Remote Purchase

#1
Solidity Example – Safe Remote Purchase

<div>
<div class="kk-star-ratings kksr-auto kksr-align-left kksr-valign-top" data-payload="{&quot;align&quot;:&quot;left&quot;,&quot;id&quot;:&quot;680392&quot;,&quot;slug&quot;:&quot;default&quot;,&quot;valign&quot;:&quot;top&quot;,&quot;ignore&quot;:&quot;&quot;,&quot;reference&quot;:&quot;auto&quot;,&quot;class&quot;:&quot;&quot;,&quot;count&quot;:&quot;1&quot;,&quot;readonly&quot;:&quot;&quot;,&quot;score&quot;:&quot;5&quot;,&quot;best&quot;:&quot;5&quot;,&quot;gap&quot;:&quot;5&quot;,&quot;greet&quot;:&quot;Rate this post&quot;,&quot;legend&quot;:&quot;5\/5 - (1 vote)&quot;,&quot;size&quot;:&quot;24&quot;,&quot;width&quot;:&quot;142.5&quot;,&quot;_legend&quot;:&quot;{score}\/{best} - ({count} {votes})&quot;,&quot;font_factor&quot;:&quot;1.25&quot;}">
<div class="kksr-stars">
<div class="kksr-stars-inactive">
<div class="kksr-star" data-star="1" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" data-star="2" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" data-star="3" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" data-star="4" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" data-star="5" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
</p></div>
<div class="kksr-stars-active" style="width: 142.5px;">
<div class="kksr-star" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
<div class="kksr-star" style="padding-right: 5px">
<div class="kksr-icon" style="width: 24px; height: 24px;"></div>
</p></div>
</p></div>
</div>
<div class="kksr-legend" style="font-size: 19.2px;"> 5/5 – (1 vote) </div>
</div>
<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube"><a href="https://blog.finxter.com/solidity-example-safe-remote-purchase/"><img src="https://blog.finxter.com/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FSZ-agoVo67A%2Fhqdefault.jpg" alt="YouTube Video"></a><figcaption></figcaption></figure>
<p>This article continues on the <a href="https://blog.finxter.com/top-solidity-smart-contract-examples-for-learning/" data-type="URL" data-id="https://blog.finxter.com/top-solidity-smart-contract-examples-for-learning/" target="_blank" rel="noreferrer noopener">Solidity Smart Contract Examples</a> series, which implements a simple, but the useful process of safe remote purchase. </p>
<p>Here, we’re walking through an example of a blind auction (<a href="https://docs.soliditylang.org/en/v0.8.15/solidity-by-example.html#safe-remote-purchase" data-type="URL" data-id="https://docs.soliditylang.org/en/v0.8.15/solidity-by-example.html#safe-remote-purchase" target="_blank" rel="noreferrer noopener">docs<a href="https://docs.soliditylang.org/en/v0.8.15/solidity-by-example.html#safe-remote-purchase"></a></a>). </p>
<ul>
<li>We’ll first lay out the entire <a rel="noreferrer noopener" href="https://blog.finxter.com/introduction-to-smart-contracts-and-solidity-part-3-blockchain-basics/" data-type="post" data-id="537705" target="_blank">smart contract</a> example without the comments for readability and development purposes. </li>
<li>Then we’ll dissect it part by part, analyze it and explain it. </li>
<li>Following this path, we’ll get a hands-on experience with smart contracts, as well as good practices in coding, understanding, and debugging smart contracts.</li>
</ul>
<h2>Smart Contract – Safe Remote Purchase</h2>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase { uint public value; address payable public seller; address payable public buyer; enum State { Created, Locked, Release, Inactive } State public state; modifier condition(bool condition_) { require(condition_); _; } error OnlyBuyer(); error OnlySeller(); error InvalidState(); error ValueNotEven(); modifier onlyBuyer() { if (msg.sender != buyer) revert OnlyBuyer(); _; } modifier onlySeller() { if (msg.sender != seller) revert OnlySeller(); _; } modifier inState(State state_) { if (state != state_) revert InvalidState(); _; } event Aborted(); event PurchaseConfirmed(); event ItemReceived(); event SellerRefunded(); constructor() payable { seller = payable(msg.sender); value = msg.value / 2; if ((2 * value) != msg.value) revert ValueNotEven(); } function abort() external onlySeller inState(State.Created) { emit Aborted(); state = State.Inactive; seller.transfer(address(this).balance); } function confirmPurchase() external inState(State.Created) condition(msg.value == (2 * value)) payable { emit PurchaseConfirmed(); buyer = payable(msg.sender); state = State.Locked; } function confirmReceived() external onlyBuyer inState(State.Locked) { emit ItemReceived(); state = State.Release; buyer.transfer(value); } function refundSeller() external onlySeller inState(State.Release) { emit SellerRefunded(); state = State.Inactive; seller.transfer(3 * value); }
}
</pre>
<h2><a></a>Code breakdown and analysis</h2>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
</pre>
<p>The state variables for recording the value, seller, and buyer addresses.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> uint public value; address payable public seller; address payable public buyer;
</pre>
<p>For the <a href="https://blog.finxter.com/solidity-crash-course/" data-type="post" data-id="445146" target="_blank" rel="noreferrer noopener">first time</a>, we’re introducing the <code>enum</code> data structure that symbolically defines the four possible states of our contract. The states are internally indexed from <code>0</code> to <code>enum_length - 1</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> enum State { Created, Locked, Release, Inactive }</pre>
<p>The variable state keeps track of the current state. Our contract starts by default in the created state and can transition to the Locked, Release, and Inactive state.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> State public state;</pre>
<p>The <code>condition</code> modifier guards a function against executing without previously satisfying the condition, i.e. an expression given alongside the function definition.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> modifier condition(bool condition_) { require(condition_); _; }
</pre>
<p>The error definitions are used with the appropriate, equally-named modifiers.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> error OnlyBuyer(); error OnlySeller(); error InvalidState(); error ValueNotEven();
</pre>
<p>The <code>onlyBuyer</code> modifier guards a function against executing when the function caller is not the buyer.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> modifier onlyBuyer() { if (msg.sender != buyer) revert OnlyBuyer(); _; }
</pre>
<p>The <code>onlySeller</code> modifier guards a function against executing when the function caller differs from the seller.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> modifier onlySeller() { if (msg.sender != seller) revert OnlySeller(); _; }
</pre>
<p>The <code>inState</code> modifier guards a function against executing when the contract state differs from the required <code>state_</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> modifier inState(State state_) { if (state != state_) revert InvalidState(); _; }
</pre>
<p>The events that the contract emits to acknowledge the functions <code>abort()</code>, <code>confirmPurchase()</code>, <code>confirmReceived()</code>, and <code>refundSeller()</code> were executed.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> event Aborted(); event PurchaseConfirmed(); event ItemReceived(); event SellerRefunded();
</pre>
<p>The constructor is declared as <code><a href="https://blog.finxter.com/what-is-payable-in-solidity/" data-type="post" data-id="37282" target="_blank" rel="noreferrer noopener">payable</a></code>, meaning that the contract deployment (synonyms <em>creation</em>, <em>instantiation</em>) requires sending a value (<code>msg.value</code>) with the contract-creating transaction.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> constructor() payable {</pre>
<p>The <code>seller</code> state variable is set to <code>msg.sender</code> address, cast (converted) to payable.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> seller = payable(msg.sender);</pre>
<p>The value state variable is set to half the <code>msg.value</code>, because both the seller and the buyer have to put twice the value of the item being sold/bought into the contract as an escrow agreement.</p>
<p class="has-global-color-8-background-color has-background"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f4a1.png" alt="?" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Info</strong>: <em>“Escrow is a legal arrangement in which a third party temporarily holds money or property until a particular condition has been met (such as the fulfillment of a purchase agreement).”</em> (<a rel="noreferrer noopener" href="https://www.rocketmortgage.com/learn/what-is-escrow" data-type="URL" data-id="https://www.rocketmortgage.com/learn/what-is-escrow" target="_blank">source</a>) </p>
<p>In our case, our escrow is our smart contract.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> value = msg.value / 2;</pre>
<p>If the value is not equally divided, i.e. the <code>msg.value</code> is not an even number, the function will terminate. Since the seller will always</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> if ((2 * value) != msg.value) revert ValueNotEven(); }
</pre>
<p>Aborting the remote safe purchase is allowed only in the <code>Created</code> state and only by the seller. </p>
<p>The <code>external</code> keyword makes the function callable only by other accounts / smart contracts. From the business perspective, only the seller can call the <code>abort()</code> function and only before the buyer decides to purchase, i.e. before the contract enters the <code>Locked</code> state.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> function abort() external onlySeller inState(State.Created) {
</pre>
<p>Emits the <code>Aborted</code> event, the contract state transitions to inactive, and the balance is transferred to the seller.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> emit Aborted(); state = State.Inactive;
</pre>
<p class="has-global-color-8-background-color has-background"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f4a1.png" alt="?" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Note</strong>: <em>“Prior to version 0.5.0, Solidity allowed address members to be accessed by a contract instance, for example, this.balance. This is now forbidden and an explicit conversion to address must be done: address(this).balance.”</em> (<a href="https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html">docs</a>). </p>
<p>In other words, this keyword lets us access the contract’s inherited members. </p>
<p>Every contract inherits its members from the address type and can access these members via <code>address(this).&lt;a member></code> (<a href="https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#address-related" data-type="URL" data-id="https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#address-related" target="_blank" rel="noreferrer noopener">docs</a>).</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> seller.transfer(address(this).balance); }
</pre>
<p>The <code>confirmPurchase()</code> function is available for execution only in the <code>Created</code> state. </p>
<p>It enforces the rule that a <code>msg.value</code> must be twice the value of the purchase. </p>
<p>The <code>confirmPurchase()</code> function is also declared as <code>payable</code>, meaning the caller, i.e. the buyer has to send the currency (<code>msg.value</code>) with the function call.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> function confirmPurchase() external inState(State.Created) condition(msg.value == (2 * value)) payable {
</pre>
<p>The <a href="https://blog.finxter.com/ethereum-virtual-machine-evm-message-calls-solidity-smart-contracts/" data-type="post" data-id="592250" target="_blank" rel="noreferrer noopener">event</a> <code>PurchaseConfirmed()</code> is emitted to mark the purchase confirmation.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> emit PurchaseConfirmed();</pre>
<p>The <code>msg.sender</code> value is cast to payable and assigned to the buyer variable.</p>
<p class="has-global-color-8-background-color has-background"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f4a1.png" alt="?" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Info</strong>: Addresses are <em>non-payable</em> by design to prevent accidental payments; that’s why we have to cast an address to a payable before being able to transfer a payment.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> buyer = payable(msg.sender);</pre>
<p>The state is set to <code>Locked</code> as seller and buyer entered the contract, i.e., our digital version of an escrow agreement.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> state = State.Locked; }
</pre>
<p>The <code>confirmReceived()</code> function is available for execution only in the <code>Locked</code> state, and only to the buyer. </p>
<p>Since the buyer deposited twice the value amount and withdrew only a single value amount, the second value amount remains on the contract balance with the seller’s deposit.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> function confirmReceived() external onlyBuyer inState(State.Locked) {
</pre>
<p>Emits the <code>ItemReceived()</code> event.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> emit ItemReceived();</pre>
<p>Changes the state to Release.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> state = State.Release;</pre>
<p>Transfers the deposit to the buyer.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> buyer.transfer(value); }
</pre>
<p>The <code>refundSeller()</code> function is available for execution only in the <code>Release</code> state, and only to the seller. </p>
<p>Since the seller deposited twice the value amount and earned a single value amount from the purchase, the contract transfers three value amounts from the contract balance to the seller.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> function refundSeller() external onlySeller inState(State.Release) {
</pre>
<p>Emits the <code>SellerRefunded()</code> event.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> emit SellerRefunded();</pre>
<p>Changes the state to <code>Inactive</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> state = State.Inactive;</pre>
<p>Transfers the deposit of two value amounts and the one earned value amount to the seller.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""> seller.transfer(3 * value); }
}
</pre>
<p>Our <a href="https://blog.finxter.com/top-solidity-smart-contract-examples-for-learning/" data-type="post" data-id="663675" target="_blank" rel="noreferrer noopener">smart contract example</a> of a safe remote purchase is a nice and simple example that demonstrates how a purchase may be conducted on the <a href="https://blog.finxter.com/smart-contracts-and-evm/" data-type="post" data-id="92507" target="_blank" rel="noreferrer noopener">Ethereum blockchain</a> network. </p>
<p>The safe remote purchase example shows two parties, a seller and a buyer, who both enter a trading relationship with their deposits to the contract balance. </p>
<p>Each deposit amounts to twice the value of the purchase, meaning that the contract balance will hold four times the purchase value at its highest point, i.e. in the <code>Locked</code> state.</p>
<p>The height of deposits is intended to stimulate the resolution of any possible disputes between the parties, because otherwise, their deposits will stay locked and unavailable in the contract balance. </p>
<p>When the buyer confirms that he received the goods he purchased, the contract will transition to the <code>Release</code> state, and the purchase value will be released to the buyer.</p>
<p>The seller can now withdraw his earned purchase value with the deposit, the contract balance drops to 0 Wei, the contract transitions to the <code>Inactive</code> state, and the safe remote purchase concludes with execution.</p>
<h2>The Contract Arguments</h2>
<p>This section contains additional information for running the contract. We should expect that our example accounts may change with each refresh/reload of Remix.</p>
<p>Our contract creation argument is the <em>deposit </em>(twice the purchase value). We’ll assume the purchase value to be 5 Wei, making the contract creation argument very simple:</p>
<p><code>10</code></p>
<h2><a></a>Contract Test Scenario</h2>
<ol type="1">
<li>Account <code>0x5B38Da6a701c568545dCfcB03FcB875f56beddC4</code> deploys the contract with a deposit of 10 Wei, effectively becoming a seller.</li>
<li>Account <code>0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2</code> confirms the purchase by calling the <code>confirmPurchase()</code> function and enters the trade with a deposit of 10 Wei, effectively becoming a buyer.</li>
<li>The buyer confirms receiving the order by calling the <code>confirmReceived()</code> function.</li>
<li>The seller concludes the trade by calling the <code>refundSeller()</code> function.</li>
</ol>
<h2><a></a>Conclusion</h2>
<p>We continued our smart contract example series with this article that implements a safe remote purchase.</p>
<p>First, we laid out clean source code (without any comments) for readability purposes.</p>
<p>Second, we dissected the code, analyzed it, and explained each possibly non-trivial segment.</p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://academy.finxter.com/university/solidity-building-blocks-an-advanced-introduction-towards-smart-contract-development-on-ethereum/" target="_blank" rel="noopener"><img loading="lazy" src="https://blog.finxter.com/wp-content/uploads/2022/05/image-291.png" alt="" class="wp-image-387294" width="363" height="650" srcset="https://blog.finxter.com/wp-content/uploads/2022/05/image-291.png 363w, https://blog.finxter.com/wp-content/uplo...68x300.png 168w" sizes="(max-width: 363px) 100vw, 363px" /></a></figure>
</div>
</div>


https://www.sickgaming.net/blog/2022/09/...-purchase/
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

Forum software by © MyBB Theme © iAndrew 2016